#define BLOB_IMPLEMENTATION
#include "blob.h"
#include "qjs_blob.h"

// Get countof from macros if not defined
#ifndef countof
#define countof(x) (sizeof(x)/sizeof((x)[0]))
#endif

// Free function for blob
void blob_free(JSRuntime *rt, blob *b)
{
  if (b) {
    blob_destroy(b);
  }
}

// Use QJSCLASS macro to generate class boilerplate
QJSCLASS(blob,)

// Constructor function for blob
static JSValue js_blob_constructor(JSContext *ctx, JSValueConst new_target,
                                  int argc, JSValueConst *argv) {
  blob *bd = NULL;

  // new Blob()
  if (argc == 0) {
    // empty antestone blob
    bd = blob_new(0);
  }
  // new Blob(capacity)
  else if (argc == 1 && JS_IsNumber(argv[0])) {
    int64_t capacity_bits;
    if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) {
      return JS_EXCEPTION;
    }
    if (capacity_bits < 0) capacity_bits = 0;
    bd = blob_new((size_t)capacity_bits);
  }
  // new Blob(length, logical/random)
  else if (argc == 2 && JS_IsNumber(argv[0])) {
    int64_t length_bits;
    if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) {
      return JS_EXCEPTION;
    }
    if (length_bits < 0) length_bits = 0;
    
    if (JS_IsBool(argv[1])) {
      // Fill with all 0s or all 1s
      int is_one = JS_ToBool(ctx, argv[1]);
      bd = blob_new_with_fill((size_t)length_bits, is_one);
    } else if (JS_IsFunction(ctx, argv[1])) {
      /* Random function provided – call it and use up to 56 bits at a time */
      size_t bytes = (length_bits + 7) / 8;
      bd = blob_new((size_t)length_bits);
      if (bd) {
        bd->length = length_bits;
        /* Ensure the backing storage starts out zeroed */
        memset(bd->data, 0, bytes);
        
        size_t bits_written = 0;
        while (bits_written < length_bits) {
          JSValue randval = JS_Call(ctx, argv[1], JS_NULL, 0, NULL);
          if (JS_IsException(randval)) {
            blob_destroy(bd);
            return JS_EXCEPTION;
          }
    
          int64_t fitval;
          JS_ToInt64(ctx, &fitval, randval);
          JS_FreeValue(ctx, randval);
          
          /* Extract up to 56 bits from the random value */
          size_t bits_to_use = length_bits - bits_written;
          if (bits_to_use > 52) bits_to_use = 52;
          
          /* Write bits from the random value */
          for (size_t j = 0; j < bits_to_use; j++) {
            size_t bit_pos = bits_written + j;
            size_t byte_idx = bit_pos / 8;
            size_t bit_idx = bit_pos % 8;
            
            if (fitval & (1LL << j))
              bd->data[byte_idx] |= (uint8_t)(1 << bit_idx);
            else
              bd->data[byte_idx] &= (uint8_t)~(1 << bit_idx);
          }
          
          bits_written += bits_to_use;
        }
      }
    } else {
      return JS_ThrowTypeError(ctx, "Second argument must be boolean or random function");
    }
  }
  // new Blob(blob, from, to)
  else if (argc >= 1 && JS_IsObject(argv[0])) {
    // we try copying from another blob if it's of the same class
    blob *src = js2blob(ctx, argv[0]);
    if (!src) {
      return JS_ThrowTypeError(ctx, "Blob constructor: argument 1 not a blob");
    }
    int64_t from = 0, to = (int64_t)src->length;
    if (argc >= 2 && JS_IsNumber(argv[1])) {
      JS_ToInt64(ctx, &from, argv[1]);
      if (from < 0) from = 0;
    }
    if (argc >= 3 && JS_IsNumber(argv[2])) {
      JS_ToInt64(ctx, &to, argv[2]);
      if (to < from) to = from;
      if (to > (int64_t)src->length) to = (int64_t)src->length;
    }
    bd = blob_new_from_blob(src, (size_t)from, (size_t)to);
  }
  // else fail
  else {
    return JS_ThrowTypeError(ctx, "Blob constructor: invalid arguments");
  }

  if (!bd) {
    return JS_ThrowOutOfMemory(ctx);
  }

  return blob2js(ctx, bd);
}

// blob.write_bit(logical)
static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv) {
  if (argc < 1) {
    return JS_ThrowTypeError(ctx, "write_bit(logical) requires 1 argument");
  }
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "write_bit: not called on a blob");
  }
  
  // Handle numeric 0/1 or boolean
  int bit_val;
  if (JS_IsNumber(argv[0])) {
    int32_t num;
    JS_ToInt32(ctx, &num, argv[0]);
    if (num != 0 && num != 1) {
      return JS_ThrowTypeError(ctx, "write_bit: value must be true, false, 0, or 1");
    }
    bit_val = num;
  } else {
    bit_val = JS_ToBool(ctx, argv[0]);
  }
  
  if (blob_write_bit(bd, bit_val) < 0) {
    return JS_ThrowTypeError(ctx, "write_bit: cannot write (maybe stone or OOM)");
  }
  return JS_NULL;
}

// blob.write_blob(second_blob)
static JSValue js_blob_write_blob(JSContext *ctx, JSValueConst this_val,
                                  int argc, JSValueConst *argv) {
  if (argc < 1) {
    return JS_ThrowTypeError(ctx, "write_blob(second_blob) requires 1 argument");
  }
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "write_blob: not called on a blob");
  }
  blob *second = js2blob(ctx, argv[0]);
  if (!second) {
    return JS_ThrowTypeError(ctx, "write_blob: argument must be a blob");
  }
  
  if (blob_write_blob(bd, second) < 0) {
    return JS_ThrowTypeError(ctx, "write_blob: cannot write to stone blob or OOM");
  }
  
  return JS_NULL;
}

// blob.write_dec64(number)
static JSValue js_blob_write_number(JSContext *ctx, JSValueConst this_val,
                                   int argc, JSValueConst *argv) {
  if (argc < 1) {
    return JS_ThrowTypeError(ctx, "write_dec64(number) requires 1 argument");
  }
  blob *bd = js2blob(ctx, this_val);
  if (!bd)
    return JS_ThrowTypeError(ctx, "write_dec64: not called on a blob");
  
  // Get the number as a double and convert to DEC64
  double d;
  if (JS_ToFloat64(ctx, &d, argv[0]) < 0)
    return JS_EXCEPTION;
  
  if (blob_write_dec64(bd, d) < 0)
    return JS_ThrowTypeError(ctx, "write_dec64: cannot write to stone blob or OOM");
  
  return JS_NULL;
}

// blob.write_fit(value, len)
static JSValue js_blob_write_fit(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv) {
  if (argc < 2)
    return JS_ThrowTypeError(ctx, "write_fit(value, len) requires 2 arguments");
    
  blob *bd = js2blob(ctx, this_val);
  if (!bd)
    return JS_ThrowTypeError(ctx, "write_fit: not called on a blob");
  
  int64_t value;
  int32_t len;
  
  if (JS_ToInt64(ctx, &value, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToInt32(ctx, &len, argv[1]) < 0) return JS_EXCEPTION;
  
  if (blob_write_fit(bd, value, len) < 0) {
    return JS_ThrowTypeError(ctx, "write_fit: value doesn't fit in specified bits, stone blob, or OOM");
  }
  
  return JS_NULL;
}

// blob.write_kim(fit)
static JSValue js_blob_write_text(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv) {
  if (argc < 1)
    return JS_ThrowTypeError(ctx, "write_kim(fit) requires 1 argument");
    
  blob *bd = js2blob(ctx, this_val);
  if (!bd)
    return JS_ThrowTypeError(ctx, "write_kim: not called on a blob");
  
  // Handle number or single character string
  const char *str = JS_ToCString(ctx, argv[0]);

  if (blob_write_text(bd, str) < 0) {
    JS_FreeCString(ctx,str);
    return JS_ThrowTypeError(ctx, "write_kim: cannot write to stone blob or OOM");
  }
  
  JS_FreeCString(ctx,str);
  return JS_NULL;
}

// blob.write_pad(block_size)
static JSValue js_blob_write_pad(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv) {
  if (argc < 1) 
    return JS_ThrowTypeError(ctx, "write_pad(block_size) requires 1 argument");

  blob *bd = js2blob(ctx, this_val);
  if (!bd) 
    return JS_ThrowTypeError(ctx, "write_pad: not called on a blob");
  
  int32_t block_size;
  if (JS_ToInt32(ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION;
  
  if (blob_write_pad(bd, block_size) < 0)
    return JS_ThrowTypeError(ctx, "write_pad: cannot write (stone blob, OOM, or invalid block size)");
  
  return JS_NULL;
}

// blob.read_logical(from)
static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val,
                                    int argc, JSValueConst *argv) {
  if (argc < 1) {
    return JS_ThrowTypeError(ctx, "read_logical(from) requires 1 argument");
  }
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "read_logical: not called on a blob");
  }
  int64_t pos;
  if (JS_ToInt64(ctx, &pos, argv[0]) < 0) {
    return JS_ThrowInternalError(ctx, "must provide a positive bit");
  }
  if (pos < 0) {
    return JS_ThrowRangeError(ctx, "read_logical: position must be non-negative");
  }
  int bit_val;
  if (blob_read_bit(bd, (size_t)pos, &bit_val) < 0) {
    return JS_ThrowTypeError(ctx, "read_logical: blob must be stone");
  }
  return JS_NewBool(ctx, bit_val);
}

// blob.read_blob(from, to)
static JSValue js_blob_read_blob(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv) {
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "read_blob: not called on a blob");
  }
  
  if (!bd->is_stone) {
    return JS_ThrowTypeError(ctx, "read_blob: blob must be stone");
  }
  
  int64_t from = 0;
  int64_t to = bd->length;
  
  if (argc >= 1) {
    if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
    if (from < 0) from = 0;
  }
  if (argc >= 2) {
    if (JS_ToInt64(ctx, &to, argv[1]) < 0) return JS_EXCEPTION;
    if (to > (int64_t)bd->length) to = bd->length;
  }
  
  blob *new_bd = blob_read_blob(bd, from, to);
  if (!new_bd) {
    return JS_ThrowOutOfMemory(ctx);
  }
  
  return blob2js(ctx, new_bd);
}

// blob.read_dec64(from)
static JSValue js_blob_read_number(JSContext *ctx, JSValueConst this_val,
                                  int argc, JSValueConst *argv) {
  if (argc < 1) {
    return JS_ThrowTypeError(ctx, "read_dec64(from) requires 1 argument");
  }
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "read_number: not called on a blob");
  }
  
  if (!bd->is_stone) {
    return JS_ThrowTypeError(ctx, "read_number: blob must be stone");
  }
  
  double from;
  if (JS_ToFloat64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
  
  if (from < 0) return JS_ThrowRangeError(ctx, "read_number: position must be non-negative");
  
  double d;
  if (blob_read_dec64(bd, from, &d) < 0) {
    return JS_ThrowRangeError(ctx, "read_number: out of range");
  }
  
  return JS_NewFloat64(ctx, d);
}

// blob.read_fit(from, len)
static JSValue js_blob_read_fit(JSContext *ctx, JSValueConst this_val,
                                int argc, JSValueConst *argv) {
  if (argc < 2) {
    return JS_ThrowTypeError(ctx, "read_fit(from, len) requires 2 arguments");
  }
  
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "read_fit: not called on a blob");
  }
  
  if (!bd->is_stone) {
    return JS_ThrowTypeError(ctx, "read_fit: blob must be stone");
  }
  
  int64_t from;
  int32_t len;
  
  if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToInt32(ctx, &len, argv[1]) < 0) return JS_EXCEPTION;
  
  if (from < 0) {
    return JS_ThrowRangeError(ctx, "read_fit: position must be non-negative");
  }
  
  int64_t value;
  if (blob_read_fit(bd, from, len, &value) < 0) {
    return JS_ThrowRangeError(ctx, "read_fit: out of range or invalid length");
  }
  
  return JS_NewInt64(ctx, value);
}

// blob.read_text(from)
static JSValue js_blob_read_text(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv) {
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "read_text: not called on a blob");
  }
  
  if (!bd->is_stone) {
    return JS_ThrowTypeError(ctx, "read_text: blob must be stone");
  }
  
  int64_t from;
  if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
  
  char *text;
  size_t bits_read;
  if (blob_read_text(bd, from, &text, &bits_read) < 0) {
    return JS_ThrowRangeError(ctx, "read_text: out of range or invalid encoding");
  }
  
  JSValue result = JS_NewString(ctx, text);
  free(text);
  
  return result;
}

// blob.pad?(from, block_size)
static JSValue js_blob_pad_q(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv) {
  if (argc < 2) {
    return JS_ThrowTypeError(ctx, "pad?(from, block_size) requires 2 arguments");
  }
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "pad?: not called on a blob");
  }
  
  if (!bd->is_stone) {
    return JS_ThrowTypeError(ctx, "pad?: blob must be stone");
  }
  
  int64_t from;
  int32_t block_size;
  if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION;
  if (JS_ToInt32(ctx, &block_size, argv[1]) < 0) return JS_EXCEPTION;
  
  return JS_NewBool(ctx, blob_pad_check(bd, from, block_size));
}

// blob.stone()
static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv) {
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "stone: not called on a blob");
  }
  if (!bd->is_stone) {
    blob_make_stone(bd);
  }
  return JS_NULL;
}

static JSValue js_blob_stonep(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv) {
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "stone: not called on a blob");
  }
  return JS_NewBool(ctx, bd->is_stone);
}

// blob.length getter
// Return number of bits in the blob
static JSValue js_blob_get_length(JSContext *ctx, JSValueConst this_val, int magic) {
  blob *bd = js2blob(ctx, this_val);
  if (!bd) {
    return JS_ThrowTypeError(ctx, "length: not called on a blob");
  }
  return JS_NewInt64(ctx, bd->length);
}

// -----------------------------------------------------------------------------
// Exports list
// -----------------------------------------------------------------------------

static const JSCFunctionListEntry js_blob_funcs[] = {
    // Write methods
    JS_CFUNC_DEF("write_bit", 1, js_blob_write_bit),
    JS_CFUNC_DEF("write_blob", 1, js_blob_write_blob),
    JS_CFUNC_DEF("write_number", 1, js_blob_write_number),
    JS_CFUNC_DEF("write_fit", 2, js_blob_write_fit),
    JS_CFUNC_DEF("write_text", 1, js_blob_write_text),
    JS_CFUNC_DEF("write_pad", 1, js_blob_write_pad),
    
    // Read methods
    JS_CFUNC_DEF("read_logical", 1, js_blob_read_logical),
    JS_CFUNC_DEF("read_blob", 2, js_blob_read_blob),
    JS_CFUNC_DEF("read_number", 1, js_blob_read_number),
    JS_CFUNC_DEF("read_fit", 2, js_blob_read_fit),
    JS_CFUNC_DEF("read_text", 1, js_blob_read_text),
    JS_CFUNC_DEF("pad?", 2, js_blob_pad_q),
    
    // Other methods
    JS_CFUNC_DEF("stone", 0, js_blob_stone),
    JS_CFUNC_DEF("stonep", 0, js_blob_stonep),
    
    // Length property getter
    JS_CGETSET_DEF("length", js_blob_get_length, NULL),
};

// -----------------------------------------------------------------------------
// js_blob_use(ctx) for easy embedding: returns an object with the blob functions
// -----------------------------------------------------------------------------

JSValue js_blob_use(JSContext *js) {
  // Register the blob class
  QJSCLASSPREP_FUNCS(blob);
  
  // Create the constructor function
  JSValue ctor = JS_NewCFunction2(js, js_blob_constructor, "blob", 3, JS_CFUNC_constructor, 0);
  
  // Set the prototype on the constructor
  JSValue proto = JS_GetClassProto(js, js_blob_id);
  JS_SetConstructor(js, ctor, proto);
  JS_FreeValue(js, proto);
  
  return ctor;
}

JSValue js_new_blob_stoned_copy(JSContext *js, void *data, size_t bytes)
{
  blob *b = blob_new(bytes*8);
  memcpy(b->data, data, bytes);
  b->length = bytes * 8;  // Set the actual length in bits
  blob_make_stone(b);
  
  return blob2js(js, b);
}

void *js_get_blob_data(JSContext *js, size_t *size, JSValue v)
{
  blob *b = js2blob(js, v);
  if (!b || !b->is_stone)
    return NULL;
    
  *size = (b->length + 7) / 8;  // Return actual byte size based on bit length
  return b->data;
}

int js_is_blob(JSContext *js, JSValue v)
{
  blob *b = js2blob(js,v);
  if (b) return 1;
  return 0;
}
