// vim:fileencoding=utf-8:foldmethod=marker

#include "str8.h"
#include "../../array/array.h"
#include "../../mem/allocator/mem_allocator.h"
#include "../../../common/aliases/aliases.h"
#include "../../../common/assert/assert.h"
#include <ctype.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

#define STR8_BUF_ALLOC_SIZE(CAPACITY) (sizeof(Str8) + sizeof(c8) * CAPACITY)

Str8 *wapp_str8_alloc_buf(const Allocator *allocator, u64 capacity) {
  wapp_debug_assert(allocator != NULL, "`allocator` should not be NULL");

  Str8 *str = wapp_mem_allocator_alloc(allocator, STR8_BUF_ALLOC_SIZE(capacity));
  if (!str) {
    goto RETURN_STR8;
  }

  str->buf      = (u8 *)str + sizeof(Str8);
  str->size     = 0;
  str->capacity = capacity;

RETURN_STR8:
  return str;
}

Str8 *wapp_str8_alloc_and_fill_buf(const Allocator *allocator, u64 capacity) {
  Str8 *out = wapp_str8_alloc_buf(allocator, capacity);
  if (out) {
    memset(out->buf, 0, capacity);
    out->size = capacity;
  }
  return out;
}

Str8 *wapp_str8_alloc_cstr(const Allocator *allocator, const char *str) {
  wapp_debug_assert(allocator != NULL && str != NULL, "`allocator` and `str` should not be NULL");

  u64 length = strlen(str);
  Str8 *output     = wapp_str8_alloc_buf(allocator, length * 2);
  if (!output) {
    goto RETURN_ALLOC_CSTR;
  }

  output->size = length;
  memcpy(output->buf, str, length);

RETURN_ALLOC_CSTR:
  return output;
}

Str8 *wapp_str8_alloc_str8(const Allocator *allocator, Str8RO *str) {
  wapp_debug_assert(allocator != NULL && str != NULL, "`allocator` and `str` should not be NULL");

  Str8 *output = wapp_str8_alloc_buf(allocator, str->capacity);
  if (!output) {
    goto RETURN_ALLOC_STR8;
  }

  output->size = str->size;
  memcpy(output->buf, str->buf, str->size);

RETURN_ALLOC_STR8:
  return output;
}

Str8 *wapp_str8_alloc_substr(const Allocator *allocator, Str8RO *str, u64 start, u64 end) {
  wapp_debug_assert(allocator != NULL && str != NULL, "`allocator` and `str` should not be NULL");

  Str8 *output = NULL;

  if (start >= str->size || start >= end) {
    goto RETURN_ALLOC_SUBSTR;
  }

  if (end > str->size) {
    end = str->size;
  }

  output = wapp_str8_alloc_buf(allocator, str->capacity);
  if (!output) {
    goto RETURN_ALLOC_SUBSTR;
  }

  output->size = end - start;
  memcpy(output->buf, str->buf + start, output->size);

RETURN_ALLOC_SUBSTR:
  return output;
}

void wapp_str8_dealloc_buf(const Allocator *allocator, Str8 **str) {
  wapp_debug_assert(allocator != NULL && str != NULL && (*str) != NULL, "Either `allocator` is NULL or `str` is an invalid double pointer");
  wapp_mem_allocator_free(allocator, (void **)str, STR8_BUF_ALLOC_SIZE((*str)->capacity));
}

c8 wapp_str8_get(const Str8 *str, u64 index) {
  if (index >= str->size) {
    return '\0';
  }

  return str->buf[index];
}

void wapp_str8_set(Str8 *str, u64 index, c8 c) {
  if (index >= str->size) {
    return;
  }

  str->buf[index] = c;
}

void wapp_str8_push_back(Str8 *str, c8 c) {
  if (!(str->size < str->capacity)) {
    return;
  }

  u64 index = (str->size)++;
  wapp_str8_set(str, index, c);
}

b8 wapp_str8_equal(Str8RO *s1, Str8RO *s2) {
  if (s1->size != s2->size) {
    return false;
  }

  return wapp_str8_equal_to_count(s1, s2, s1->size);
}

b8 wapp_str8_equal_to_count(Str8RO* s1, Str8RO* s2, u64 count) {
  if (!s1 || !s2) {
      return false;
  }

  return memcmp(s1->buf, s2->buf, count) == 0;
}

Str8 wapp_str8_slice(Str8RO *str, u64 start, u64 end) {
  if (start >= str->size || start >= end) {
    start = str->size;
    end   = str->size;
  }

  if (end > str->size) {
    end = str->size;
  }

  return (Str8RO){
    .capacity = end - start,
    .size     = end - start,
    .buf      = str->buf + start,
  };
}

Str8 *wapp_str8_alloc_concat(const Allocator *allocator, Str8 *dst, Str8RO *src) {
  wapp_debug_assert(allocator != NULL && dst != NULL && src != NULL, "`allocator`, `dst` and `src` should not be NULL");

  Str8 *output  = NULL;
  u64 remaining = dst->capacity - dst->size;
  if (src->size <= remaining) {
    output = dst;
    goto SOURCE_STRING_STR8_CONCAT;
  }

  u64 capacity = dst->capacity + src->size;

  output = wapp_str8_alloc_buf(allocator, capacity);
  if (!output) {
    goto RETURN_STR8_CONCAT;
  }

  wapp_str8_concat_capped(output, dst);

SOURCE_STRING_STR8_CONCAT:
  wapp_str8_concat_capped(output, src);

RETURN_STR8_CONCAT:
  return output;
}

void wapp_str8_concat_capped(Str8 *dst, Str8RO *src) {
  wapp_debug_assert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

  u64 remaining = dst->capacity - dst->size;
  u64 to_copy   = remaining < src->size ? remaining : src->size;

  memcpy(dst->buf + dst->size, src->buf, to_copy);
  dst->size += to_copy;
}

void wapp_str8_copy_cstr_capped(Str8 *dst, const char *src) {
  wapp_debug_assert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

  u64 length  = strlen(src);
  u64 to_copy = length <= dst->capacity ? length : dst->capacity;

  memset(dst->buf, 0, dst->size);
  memcpy(dst->buf, src, to_copy);
  dst->size = to_copy;
}

void wapp_str8_copy_str8_capped(Str8 *dst, Str8RO *src) {
  wapp_debug_assert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

  u64 to_copy = src->size <= dst->capacity ? src->size : dst->capacity;

  memset(dst->buf, 0, dst->size);
  memcpy(dst->buf, src->buf, to_copy);
  dst->size = to_copy;
}

void wapp_str8_copy_to_cstr(char *dst, Str8RO *src, u64 dst_capacity) {
  wapp_debug_assert(dst != NULL && src != NULL, "`dst` and `src` should not be NULL");

  u64 to_copy = src->size < dst_capacity ? src->size : dst_capacity - 1;

  memset(dst, 0, dst_capacity);
  memcpy(dst, src->buf, to_copy);
}

void wapp_str8_format(Str8 *dst, const char *format, ...) {
  wapp_debug_assert(dst != NULL && format != NULL, "`dst` and `format` should not be NULL");

  va_list args1;
  va_list args2;

  va_start(args1, format);
  va_copy(args2, args1);

  u64 total_size = vsnprintf(NULL, 0, format, args1);
  dst->size      = total_size <= dst->capacity ? total_size : dst->capacity;

  vsnprintf((char *)(dst->buf), dst->capacity, format, args2);

  va_end(args1);
  va_end(args2);
}

void wapp_str8_to_lower(Str8 *dst, Str8RO *src) {
  wapp_debug_assert(src != NULL && dst != NULL, "`dst` and `src` should not be NULL");
  wapp_debug_assert(dst->capacity >= src->capacity, "`dst` does not have enough capacity");

  dst->size = src->size;

  // NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
  // MSVC Spectre mitigation warnings
  u64 index  = 0;
  b8 running = true;
  while (running) {
    wapp_str8_set(dst, index, (u8)tolower(wapp_str8_get(src, index)));
    ++index;
    running = index < src->size;
  }
}

void wapp_str8_to_upper(Str8 *dst, Str8RO *src) {
  wapp_debug_assert(src != NULL && dst != NULL, "`dst` and `src` should not be NULL");
  wapp_debug_assert(dst->capacity >= src->capacity, "`dst` does not have enough capacity");

  dst->size = src->size;

  // NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
  // MSVC Spectre mitigation warnings
  u64 index  = 0;
  b8 running = true;
  while (running) {
    wapp_str8_set(dst, index, (u8)toupper(wapp_str8_get(src, index)));
    ++index;
    running = index < src->size;
  }
}

void wapp_str8_from_bytes(Str8 *dst, const U8Array *src) {
  wapp_debug_assert(src != NULL && dst != NULL, "`dst` and `src` should not be NULL");

  u64 size = src->count * src->item_size;

  wapp_debug_assert(dst->capacity >= size, "`dst` does not have enough capacity");

  dst->size = size;
  memcpy(dst->buf, src->items, size);
}

i64 wapp_str8_find(Str8RO *str, Str8RO substr) {
  if (!str || substr.size > str->size) {
    return -1;
  }

  // NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
  // MSVC Spectre mitigation warnings
  u64 char_index = 0;
  b8 running    = char_index < str->size;
  while (running) {
    const c8 *sub = str->buf + char_index;
    if (memcmp(sub, substr.buf, substr.size) == 0) {
      return char_index;
    }

    ++char_index;
    running = char_index < str->size;
  }

  return -1;
}

i64 wapp_str8_rfind(Str8RO *str, Str8RO substr) {
  if (!str || substr.size > str->size) {
    return -1;
  }

  // NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
  // MSVC Spectre mitigation warnings
  i64 char_index = str->size - substr.size;
  b8 running    = char_index >= 0;
  while (running) {
    const c8 *sub = str->buf + char_index;
    if (memcmp(sub, substr.buf, substr.size) == 0) {
      return char_index;
    }

    --char_index;
    running = char_index >= 0;
  }

  return -1;
}

Str8List *wapp_str8_split_with_max(const Allocator *allocator, Str8RO *str, Str8RO *delimiter, i64 max_splits) {
  wapp_debug_assert(allocator != NULL && str != NULL && delimiter != NULL, "`allocator`, `str` and `delimiter` should not be NULL");

  Str8List *output = wapp_dbl_list_alloc(Str8, Str8List, allocator);

  if (delimiter->size > str->size) {
    Str8 *full     = wapp_str8_alloc_str8(allocator, str);
    Str8Node *node = wapp_dbl_list_node_alloc(Str8, Str8Node, allocator);
    if (node) {
      node->item = full;
      wapp_dbl_list_push_back(Str8, output, node);
    }

    goto RETURN_STR8_SPLIT;
  }

  i64 start   = 0;
  i64 end     = 0;
  i64 splits  = 0;
  Str8 *rest = wapp_str8_alloc_str8(allocator, str);
  Str8 *before_str;

  while ((end = wapp_str8_find(rest, *delimiter)) != -1) {
    if (max_splits > 0 && splits >= max_splits) {
      break;
    }

    before_str     = wapp_str8_alloc_substr(allocator, str, start, start + end);
    Str8Node *node = wapp_dbl_list_node_alloc(Str8, Str8Node, allocator);
    if (node && before_str) {
      node->item = before_str;
      wapp_dbl_list_push_back(Str8, output, node);
    }

    wapp_mem_allocator_free(allocator, (void **)&rest, sizeof(Str8));
    rest   = wapp_str8_alloc_substr(allocator, str, start + end + delimiter->size, str->size);
    start += end + delimiter->size;

    ++splits;
  }

  // Ensure the last part of the string after the delimiter is added to the list
  rest           = wapp_str8_alloc_substr(allocator, str, start, str->size);
  Str8Node *node = wapp_dbl_list_node_alloc(Str8, Str8Node, allocator);
  if (node && rest) {
    node->item = rest;
    wapp_dbl_list_push_back(Str8, output, node);
  }

RETURN_STR8_SPLIT:
  return output;
}

Str8List *wapp_str8_rsplit_with_max(const Allocator *allocator, Str8RO *str, Str8RO *delimiter, i64 max_splits) {
  wapp_debug_assert(allocator != NULL && str != NULL && delimiter != NULL, "`allocator`, `str` and `delimiter` should not be NULL");

  Str8List *output = wapp_dbl_list_alloc(Str8, Str8List, allocator);

  if (delimiter->size > str->size) {
    Str8 *full     = wapp_str8_alloc_str8(allocator, str);
    Str8Node *node = wapp_dbl_list_node_alloc(Str8, Str8Node, allocator);
    if (node && full) {
      node->item = full;
      wapp_dbl_list_push_back(Str8, output, node);
    }

    goto RETURN_STR8_SPLIT;
  }

  i64 end     = 0;
  i64 splits  = 0;
  Str8 *rest = wapp_str8_alloc_str8(allocator, str);
  Str8 *after_str;

  while ((end = wapp_str8_rfind(rest, *delimiter)) != -1) {
    if (max_splits > 0 && splits >= max_splits) {
      break;
    }

    after_str     = wapp_str8_alloc_substr(allocator, rest, end + delimiter->size, str->size);
    Str8Node *node = wapp_dbl_list_node_alloc(Str8, Str8Node, allocator);
    if (node) {
      node->item = after_str;
      wapp_dbl_list_push_front(Str8, output, node);
    }

    wapp_mem_allocator_free(allocator, (void **)&rest, sizeof(Str8));
    rest   = wapp_str8_alloc_substr(allocator, rest, 0, end);

    ++splits;
  }

  rest           = wapp_str8_alloc_substr(allocator, str, 0, rest->size);
  Str8Node *node = wapp_dbl_list_node_alloc(Str8, Str8Node, allocator);
  if (node && rest) {
    node->item = rest;
    wapp_dbl_list_push_front(Str8, output, node);
  }

RETURN_STR8_SPLIT:
  return output;
}

Str8 *wapp_str8_join(const Allocator *allocator, const Str8List *list, Str8RO *delimiter) {
  wapp_debug_assert(allocator != NULL && list != NULL && delimiter != NULL, "`allocator`, `list` and `delimiter` should not be NULL");

  u64 capacity = wapp_str8_list_total_size(list) + (delimiter->size * (list->node_count - 1));
  Str8 *output = wapp_str8_alloc_buf(allocator, capacity * 2);

  // NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
  // MSVC Spectre mitigation warnings
  Str8Node *node;
  u64 node_index = 0;
  b8 running    = node_index < list->node_count;
  while (running) {
    node = wapp_dbl_list_get(Str8, Str8Node, list, node_index);
    if (!node) {
        break;
    }

    wapp_str8_concat_capped(output, node->item);

    // NOTE (Abdelrahman): Comparison extracted to variable to silence
    // MSVC Spectre mitigation warnings
    b8 not_last = node_index + 1 < list->node_count;
    if (not_last) {
      wapp_str8_concat_capped(output, delimiter);
    }

    ++node_index;
    running = node_index < list->node_count;
  }

  return output;
}

u64 wapp_str8_list_total_size(const Str8List *list) {
  if (!list) {
    return 0;
  }

  // NOTE (Abdelrahman): Uses a while loop instead of a for loop to get rid of
  // MSVC Spectre mitigation warnings
  Str8Node* node;
  u64 node_index = 0;
  u64 output     = 0;
  b8 running    = node_index < list->node_count;
  while (running) {
      node = wapp_dbl_list_get(Str8, Str8Node, list, node_index);
      if (!node) {
          break;
      }

      output += node->item->size;
      ++node_index;
      running = node_index < list->node_count;
  }

  return output;
}
