#include "qjs_text.h"
#include "qjs_blob.h"
#include "blob.h"
#include <string.h>
#include <stdlib.h>

JSC_CCALL(text_blob_to_hex,
  size_t blob_len;
  void *blob_data = js_get_blob_data(js, &blob_len, argv[0]);
  if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob");
  uint8_t *bytes = (uint8_t *)blob_data;
  size_t hex_len = blob_len * 2;
  char *hex_str = malloc(hex_len + 1);
  if (!hex_str) return JS_ThrowOutOfMemory(js);
  static const char hex_digits[] = "0123456789ABCDEF";
  for (size_t i = 0; i < blob_len; ++i) {
    hex_str[i * 2] = hex_digits[(bytes[i] >> 4) & 0xF];
    hex_str[i * 2 + 1] = hex_digits[bytes[i] & 0xF];
  }
  hex_str[hex_len] = '\0';
  JSValue val = JS_NewString(js, hex_str);
  free(hex_str);
  return val;
)

JSC_CCALL(text_blob_to_base32,
  size_t blob_len;
  void *blob_data = js_get_blob_data(js, &blob_len, argv[0]);
  if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob");
  uint8_t *bytes = (uint8_t *)blob_data;
  static const char b32_digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  
  // Calculate exact output length needed
  size_t groups = (blob_len + 4) / 5;  // Round up to next group of 5
  size_t b32_len = groups * 8;
  char *b32_str = malloc(b32_len + 1);
  if (!b32_str) return JS_ThrowOutOfMemory(js);
  
  size_t in_idx = 0;
  size_t out_idx = 0;
  
  while (in_idx < blob_len) {
    // Read up to 5 bytes into a 40-bit buffer
    uint64_t buf = 0;
    int bytes_to_read = (blob_len - in_idx < 5) ? (blob_len - in_idx) : 5;
    
    for (int i = 0; i < bytes_to_read; ++i) {
      buf = (buf << 8) | bytes[in_idx++];
    }
    
    // Pad buffer to 40 bits if we read fewer than 5 bytes
    buf <<= 8 * (5 - bytes_to_read);
    
    // Extract 8 groups of 5 bits each
    for (int i = 0; i < 8; ++i) {
      b32_str[out_idx++] = b32_digits[(buf >> (35 - i * 5)) & 0x1F];
    }
  }
  
  // Add padding if necessary
  if (blob_len % 5 != 0) {
    static const int pad_count[] = {0, 6, 4, 3, 1};  // padding for 0,1,2,3,4 bytes
    int padding = pad_count[blob_len % 5];
    for (int i = 0; i < padding; ++i) {
      b32_str[b32_len - 1 - i] = '=';
    }
  }
  
  b32_str[b32_len] = '\0';
  JSValue val = JS_NewString(js, b32_str);
  free(b32_str);
  return val;
)

static int base32_char_to_val(char c) {
  if (c >= 'A' && c <= 'Z') return c - 'A';
  if (c >= 'a' && c <= 'z') return c - 'a';
  if (c >= '2' && c <= '7') return c - '2' + 26;
  return -1;
}

JSC_CCALL(text_base32_to_blob,
  const char *str = JS_ToCString(js, argv[0]);
  if (!str) return JS_ThrowTypeError(js, "Expected string");
  size_t str_len = strlen(str);
  if (str_len == 0) {
    JS_FreeCString(js, str);
    return JS_ThrowTypeError(js, "Empty base32 string");
  }
  
  // Remove padding to get effective length
  size_t effective_len = str_len;
  while (effective_len > 0 && str[effective_len - 1] == '=') {
    effective_len--;
  }
  
  // Calculate output length: each group of 8 base32 chars -> 5 bytes
  size_t output_len = (effective_len * 5) / 8;
  uint8_t *output = malloc(output_len);
  if (!output) {
    JS_FreeCString(js, str);
    return JS_ThrowOutOfMemory(js);
  }
  
  size_t in_idx = 0;
  size_t out_idx = 0;
  
  // Process in groups of 8 characters (40 bits -> 5 bytes)
  while (in_idx < effective_len) {
    uint64_t buf = 0;
    int chars_to_read = (effective_len - in_idx < 8) ? (effective_len - in_idx) : 8;
    
    // Read up to 8 base32 characters into buffer
    for (int i = 0; i < chars_to_read; ++i) {
      int val = base32_char_to_val(str[in_idx++]);
      if (val < 0) {
        free(output);
        JS_FreeCString(js, str);
        return JS_ThrowTypeError(js, "Invalid base32 character");
      }
      buf = (buf << 5) | val;
    }
    
    // Calculate how many bytes we can extract
    int bytes_to_extract = (chars_to_read * 5) / 8;
    
    // Shift buffer to align the most significant bits
    buf <<= (40 - chars_to_read * 5);
    
    // Extract bytes from most significant to least significant
    for (int i = 0; i < bytes_to_extract && out_idx < output_len; ++i) {
      output[out_idx++] = (buf >> (32 - i * 8)) & 0xFF;
    }
  }
  
  JSValue val = js_new_blob_stoned_copy(js, output, output_len);
  free(output);
  JS_FreeCString(js, str);
  return val;
)

static int base64_char_to_val_standard(char c) {
  if (c >= 'A' && c <= 'Z') return c - 'A';
  if (c >= 'a' && c <= 'z') return c - 'a' + 26;
  if (c >= '0' && c <= '9') return c - '0' + 52;
  if (c == '+') return 62;
  if (c == '/') return 63;
  return -1;
}
static int base64_char_to_val_url(char c) {
  if (c >= 'A' && c <= 'Z') return c - 'A';
  if (c >= 'a' && c <= 'z') return c - 'a' + 26;
  if (c >= '0' && c <= '9') return c - '0' + 52;
  if (c == '-') return 62;
  if (c == '_') return 63;
  return -1;
}

/*─── blob → Base64 (standard, with ‘+’ and ‘/’, padded) ───────────────────*/
JSC_CCALL(text_blob_to_base64,
  size_t blob_len;
  void *blob_data = js_get_blob_data(js, &blob_len, argv[0]);
  if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob");
  const uint8_t *bytes = blob_data;
  static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                            "abcdefghijklmnopqrstuvwxyz"
                            "0123456789+/";
  size_t out_len = ((blob_len + 2) / 3) * 4;
  char *out = malloc(out_len + 1);
  if (!out) return JS_ThrowOutOfMemory(js);

  size_t in_i = 0, out_i = 0;
  while (in_i < blob_len) {
    uint32_t buf = 0;
    int to_read = (blob_len - in_i < 3 ? blob_len - in_i : 3);
    for (int j = 0; j < to_read; ++j) {
      buf = (buf << 8) | bytes[in_i++];
    }
    buf <<= 8 * (3 - to_read);
    out[out_i++] = b64[(buf >> 18) & 0x3F];
    out[out_i++] = b64[(buf >> 12) & 0x3F];
    out[out_i++] = (to_read > 1 ? b64[(buf >> 6) & 0x3F] : '=');
    out[out_i++] = (to_read > 2 ? b64[ buf        & 0x3F] : '=');
  }
  out[out_len] = '\0';
  JSValue v = JS_NewString(js, out);
  free(out);
  return v;
)

/*─── Base64 → blob (standard, expects ‘+’ and ‘/’, pads allowed) ────────────*/
JSC_CCALL(text_base64_to_blob,
  const char *str = JS_ToCString(js, argv[0]);
  if (!str) return JS_ThrowTypeError(js, "Expected string");
  size_t len = strlen(str);
  // strip padding for length calculation
  size_t eff = len;
  while (eff > 0 && str[eff-1] == '=') eff--;
  size_t out_len = (eff * 6) / 8;
  uint8_t *out = malloc(out_len);
  if (!out) { JS_FreeCString(js, str); return JS_ThrowOutOfMemory(js); }

  size_t in_i = 0, out_i = 0;
  while (in_i < eff) {
    uint32_t buf = 0;
    int to_read = (eff - in_i < 4 ? eff - in_i : 4);
    for (int j = 0; j < to_read; ++j) {
      int v = base64_char_to_val_standard(str[in_i++]);
      if (v < 0) { free(out); JS_FreeCString(js, str);
                   return JS_ThrowTypeError(js, "Invalid base64 character"); }
      buf = (buf << 6) | v;
    }
    buf <<= 6 * (4 - to_read);
    int bytes_out = (to_read * 6) / 8;
    for (int j = 0; j < bytes_out && out_i < out_len; ++j) {
      out[out_i++] = (buf >> (16 - 8*j)) & 0xFF;
    }
  }

  JSValue v = js_new_blob_stoned_copy(js, out, out_len);
  free(out);
  JS_FreeCString(js, str);
  return v;
)

/*─── blob → Base64URL (no padding, ‘-’ and ‘_’) ─────────────────────────────*/
JSC_CCALL(text_blob_to_base64url,
  size_t blob_len;
  void *blob_data = js_get_blob_data(js, &blob_len, argv[0]);
  if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob");
  const uint8_t *bytes = blob_data;
  static const char b64url[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                               "abcdefghijklmnopqrstuvwxyz"
                               "0123456789-_";
  size_t raw_len = ((blob_len + 2) / 3) * 4;
  // we’ll drop any trailing '='
  char *out = malloc(raw_len + 1);
  if (!out) return JS_ThrowOutOfMemory(js);

  size_t in_i = 0, out_i = 0;
  while (in_i < blob_len) {
    uint32_t buf = 0;
    int to_read = (blob_len - in_i < 3 ? blob_len - in_i : 3);
    for (int j = 0; j < to_read; ++j) {
      buf = (buf << 8) | bytes[in_i++];
    }
    buf <<= 8 * (3 - to_read);
    out[out_i++] = b64url[(buf >> 18) & 0x3F];
    out[out_i++] = b64url[(buf >> 12) & 0x3F];
    if (to_read > 1) out[out_i++] = b64url[(buf >> 6) & 0x3F];
    if (to_read > 2) out[out_i++] = b64url[ buf        & 0x3F];
  }
  out[out_i] = '\0';
  JSValue v = JS_NewString(js, out);
  free(out);
  return v;
)

/*─── Base64URL → blob (accepts ‘-’ / ‘_’, no padding needed) ─────────────────*/
JSC_CCALL(text_base64url_to_blob,
  const char *str = JS_ToCString(js, argv[0]);
  if (!str) return JS_ThrowTypeError(js, "Expected string");
  size_t len = strlen(str);
  size_t eff = len;  // no '=' in URL‐safe, but strip if present
  while (eff > 0 && str[eff-1] == '=') eff--;
  size_t out_len = (eff * 6) / 8;
  uint8_t *out = malloc(out_len);
  if (!out) { JS_FreeCString(js, str); return JS_ThrowOutOfMemory(js); }

  size_t in_i = 0, out_i = 0;
  while (in_i < eff) {
    uint32_t buf = 0;
    int to_read = (eff - in_i < 4 ? eff - in_i : 4);
    for (int j = 0; j < to_read; ++j) {
      int v = base64_char_to_val_url(str[in_i++]);
      if (v < 0) { free(out); JS_FreeCString(js, str);
                   return JS_ThrowTypeError(js, "Invalid base64url character"); }
      buf = (buf << 6) | v;
    }
    buf <<= 6 * (4 - to_read);
    int bytes_out = (to_read * 6) / 8;
    for (int j = 0; j < bytes_out && out_i < out_len; ++j) {
      out[out_i++] = (buf >> (16 - 8*j)) & 0xFF;
    }
  }

  JSValue v = js_new_blob_stoned_copy(js, out, out_len);
  free(out);
  JS_FreeCString(js, str);
  return v;
)

static const JSCFunctionListEntry js_text_funcs[] = {
  MIST_FUNC_DEF(text, blob_to_hex, 1),
  MIST_FUNC_DEF(text, blob_to_base32, 1),
  MIST_FUNC_DEF(text, base32_to_blob, 1),
  MIST_FUNC_DEF(text, blob_to_base64,      1),
  MIST_FUNC_DEF(text, base64_to_blob,      1),
  MIST_FUNC_DEF(text, blob_to_base64url,   1),
  MIST_FUNC_DEF(text, base64url_to_blob,   1),
};

JSValue js_text_use(JSContext *js)
{
  JSValue mod = JS_NewObject(js);
  JS_SetPropertyFunctionList(js, mod, js_text_funcs, countof(js_text_funcs));
  return mod;
}