/// Fixed-size strings with value semantics.
#pragma once

#include <cassert.h>

#ifdef __linux__
#include <bsd/string.h>
#else
#include <string.h>
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

// -----------------------------------------------------------------------------
// Fix-sized strings.

/// A fixed-size string.
/// The string is null-terminated so that it can be used with the usual C APIs.
#define DEF_STRING(STRING, SIZE)                                              \
  typedef struct STRING {                                                     \
    size_t length;                                                            \
    char   str[SIZE];                                                         \
  } STRING;                                                                   \
                                                                              \
  static const size_t STRING##_size = SIZE;                                   \
                                                                              \
  static inline const char* STRING##_cstr(const STRING* str) {                \
    return str->str;                                                          \
  }                                                                           \
                                                                              \
  static inline size_t STRING##_length(const STRING* str) {                   \
    return str->length;                                                       \
  }                                                                           \
                                                                              \
  static inline STRING STRING##_make(const char* cstr) {                      \
    if (!cstr) {                                                              \
      return (STRING){0};                                                     \
    } else {                                                                  \
      STRING str = (STRING){0};                                               \
      str.length = strlcpy(str.str, cstr, SIZE);                              \
      return str;                                                             \
    }                                                                         \
  }                                                                           \
                                                                              \
  static inline STRING STRING##_make_empty() { return (STRING){0}; }          \
                                                                              \
  static inline STRING STRING##_dirname(const STRING path) {                  \
    STRING str = path;                                                        \
    for (int i = str.length - 1; i >= 0; --i) {                               \
      if (str.str[i] == '/' || str.str[i] == '\\') {                          \
        str.str[i] = 0;                                                       \
        str.length = i;                                                       \
        return str;                                                           \
      } else {                                                                \
        str.str[i] = 0;                                                       \
      }                                                                       \
    }                                                                         \
    str        = (STRING){0};                                                 \
    str.str[0] = '.';                                                         \
    str.length = 1;                                                           \
    return str;                                                               \
  }                                                                           \
                                                                              \
  static inline void STRING##_append_cstr_len(                                \
      STRING* a, const char* b, const size_t b_length) {                      \
    ASSERT(a->length + b_length + 1 <= SIZE);                                 \
    strlcpy(a->str + a->length, b, SIZE - a->length);                         \
    a->length += b_length;                                                    \
  }                                                                           \
                                                                              \
  static inline void STRING##_append_cstr(STRING* a, const char* b) {         \
    STRING##_append_cstr_len(a, b, strlen(b));                                \
  }                                                                           \
                                                                              \
  static inline void STRING##_append(STRING* a, const STRING b) {             \
    STRING##_append_cstr_len(a, b.str, b.length);                             \
  }                                                                           \
                                                                              \
  static inline STRING STRING##_concat_cstr_len(                              \
      const STRING a, const char* b, const size_t b_length) {                 \
    ASSERT(a.length + b_length + 1 <= SIZE);                                  \
    STRING str = {0};                                                         \
    STRING##_append_cstr_len(&str, a.str, a.length);                          \
    STRING##_append_cstr_len(&str, b, b_length);                              \
    return str;                                                               \
  }                                                                           \
                                                                              \
  static inline STRING STRING##_concat_cstr(const STRING a, const char* b) {  \
    return STRING##_concat_cstr_len(a, b, strlen(b));                         \
  }                                                                           \
                                                                              \
  static inline STRING STRING##_concat(const STRING a, const STRING b) {      \
    return STRING##_concat_cstr_len(a, b.str, b.length);                      \
  }                                                                           \
                                                                              \
  static inline STRING STRING##_concat_path(const STRING a, const STRING b) { \
    return STRING##_concat(STRING##_concat_cstr(a, "/"), b);                  \
  }                                                                           \
                                                                              \
  static inline STRING STRING##_slice(                                        \
      const STRING a, const size_t start, const size_t end) {                 \
    ASSERT(start < a.length);                                                 \
    ASSERT(end <= a.length);                                                  \
    STRING str = {0};                                                         \
    strlcpy(str.str, &a.str[start], end - start);                             \
    return str;                                                               \
  }                                                                           \
                                                                              \
  static inline bool STRING##_eq_cstr_len(                                    \
      const STRING a, const char* b, size_t b_length) {                       \
    return (a.length == b_length) && strncmp(a.str, b, a.length) == 0;        \
  }                                                                           \
                                                                              \
  static inline bool STRING##_eq_cstr(const STRING a, const char* b) {        \
    return STRING##_eq_cstr_len(a, b, strlen(b));                             \
  }                                                                           \
                                                                              \
  static inline bool STRING##_eq(const STRING a, const STRING b) {            \
    return STRING##_eq_cstr_len(a, b.str, b.length);                          \
  }                                                                           \
                                                                              \
  static inline bool STRING##_empty(const STRING a) { return a.length == 0; } \
                                                                              \
  static inline STRING STRING##_itoa(int n) {                                 \
    STRING    str     = (STRING){0};                                          \
    const int written = snprintf(str.str, SIZE, "%d", n);                     \
    ASSERT(written >= 0);                                                     \
    str.length = (size_t)written;                                             \
    return str;                                                               \
  }                                                                           \
                                                                              \
  static inline uint64_t STRING##_hash(const STRING str) {                    \
    return cstring_hash(str.str);                                             \
  }

/// Return a hash of the given string.
static inline uint64_t cstring_hash(const char* str) {
  uint64_t hash = 0;
  for (size_t i = 0; i < strlen(str); ++i) {
    hash = (uint64_t)str[i] + (hash << 6) + (hash << 16) - hash;
  }
  return hash;
}

DEF_STRING(sstring, 32)    // Small.
DEF_STRING(mstring, 256)   // Medium.
DEF_STRING(lstring, 1024)  // Large.
DEF_STRING(xlstring, 4096) // Extra large.

// -----------------------------------------------------------------------------
// Dynamically-sized strings.

typedef struct string {
  const char* data;
  size_t      length;
} string;

/// Create a new string.
string string_new(const char*);

/// Delete the string.
void string_del(string*);

/// Get the string's length.
static inline size_t string_length(const string str) { return str.length; }

/// Get the string's data.
static inline const char* string_data(const string str) { return str.data; }

/// Concatenate two strings.
string string_concat(string left, string right);

/// Convert a size to string.
string string_from_size(size_t);

/// Convert and format a size to string.
/// The result uses B for bytes, K for kilobytes (1024), M for megabytes
/// (2**20), and G for gigabytes (2**30), with two decimal digits.
string string_format_size(size_t);