#include "cell.h"
#include "qjs_blob.h"

#include "quirc.h"
#include "qrencode.h"

#include <math.h>    // for sqrt
#include <stdlib.h>  // for size_t, etc.
#include <string.h>  // for memcpy, strcmp
#include <stdint.h>

#include <errno.h>

// QR encode function
static JSValue js_qr_encode(JSContext *js, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1) {
        return JS_ThrowTypeError(js, "encode expects a string or ArrayBuffer as first argument");
    }

    // Default options
    int version = 0;  // 0 means auto-select in qrencode
    int errc = QR_ECLEVEL_L;  // Default to "L"
    int casesensitive = 1;  // Default to true (qrencode treats 1 as case-sensitive)
    int use_byte_mode = 0;

    // Handle options object (argv[1])
    if (argc > 1 && !JS_IsNull(argv[1]) && JS_IsObject(argv[1])) {
        JSValue opt = argv[1];
        
                JSValue m = JS_GetPropertyStr(js, opt, "mode");
                if (!JS_IsNull(m)) {
                    const char *smode = JS_ToCString(js, m);
                    if (!smode) { JS_FreeValue(js, m); return JS_EXCEPTION; }
                    if (!strcasecmp(smode, "byte") || !strcasecmp(smode, "binary"))
                        use_byte_mode = 1;
                    else if (!strcasecmp(smode, "text"))
                        use_byte_mode = 0;
                    else {
                        JS_FreeCString(js, smode); JS_FreeValue(js, m);
                        return JS_ThrowRangeError(js, "mode must be 'byte' or 'text'");
                    }
                    JS_FreeCString(js, smode);
                    JS_FreeValue(js, m);
                }

        // Version (1-40)
        JSValue v = JS_GetPropertyStr(js, opt, "version");
        if (!JS_IsNull(v)) {
            int32_t ver;
            if (JS_ToInt32(js, &ver, v) || ver < 0 || ver > 40) {
                JS_FreeValue(js, v);
                return JS_ThrowRangeError(js, "version must be between 0 and 40");
            }
            version = ver;
            JS_FreeValue(js, v);
        }

        // Error correction level ("l", "m", "q", "h")
        JSValue l = JS_GetPropertyStr(js, opt, "level");
        if (!JS_IsNull(l)) {
            const char *level = JS_ToCString(js, l);
            if (!level) {
                JS_FreeValue(js, l);
                return JS_ThrowTypeError(js, "level must be a string");
            }
            if (strcasecmp(level, "l") == 0) errc = QR_ECLEVEL_L;
            else if (strcasecmp(level, "m") == 0) errc = QR_ECLEVEL_M;
            else if (strcasecmp(level, "q") == 0) errc = QR_ECLEVEL_Q;
            else if (strcasecmp(level, "h") == 0) errc = QR_ECLEVEL_H;
            else {
                JS_FreeCString(js, level);
                JS_FreeValue(js, l);
                return JS_ThrowRangeError(js, "level must be 'l', 'm', 'q', or 'h'");
            }
            JS_FreeCString(js, level);
            JS_FreeValue(js, l);
        }

        // Case sensitivity (true/false)
        JSValue ci = JS_GetPropertyStr(js, opt, "caseinsensitive");
        if (!JS_IsNull(ci)) {
            int bool_val;
            if (JS_ToBool(js, ci)) {
                bool_val = 0;  // qrencode: 0 = case-insensitive
            } else {
                bool_val = 1;  // qrencode: 1 = case-sensitive 
            }
            casesensitive = bool_val;
            JS_FreeValue(js, ci);
        }
    }

    const char *data = NULL;
    size_t data_len = 0;
    int must_free_data = 0;

    // Handle string input
    if (!use_byte_mode && JS_IsString(argv[0])) {
        data = JS_ToCStringLen(js, &data_len, argv[0]);
        if (!data) {
            return JS_ThrowTypeError(js, "Invalid string input");
        }
        must_free_data = 1;
    } else {
        /* treat everything else as raw bytes */
        use_byte_mode = 1;
        uint8_t *buf = js_get_blob_data(js, &data_len, argv[0]);
        if (!buf) {
            return JS_ThrowTypeError(js, "encode expects a string or ArrayBuffer");
        }
        data = (const char *)buf;  // Cast to char* for qrencode
    }

    // Encode using qrencode
    QRcode *qrcode;
    if (use_byte_mode)
     qrcode = QRcode_encodeData(data_len, data, version, errc);
    else
      qrcode = QRcode_encodeString(data, version, errc, QR_MODE_8, casesensitive);

    if (!qrcode) {
        if (must_free_data)
            JS_FreeCString(js, data);
        return JS_ThrowInternalError(js, "Failed to encode QR code: %s", strerror(errno));
    }

    // qrcode->width is the size of one side
    // qrcode->data is width * width bytes
    printf("Encoded %d of data into a QR of width %d\n", data_len, qrcode->width);
    int width = qrcode->width;
    size_t size = (size_t)width * width; // total modules

    // Create an array of booleans for the module data
    JSValue dataArr = JS_NewArray(js);
    for (size_t i = 0; i < size; i++) {
        int is_black = (qrcode->data[i] & 1);
        JSValue val = JS_NewBool(js, is_black);
        JS_SetPropertyUint32(js, dataArr, (uint32_t)i, val);
    }

    // Build a JS object to hold { width, data }
    JSValue result = JS_NewObject(js);
    JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
    JS_SetPropertyStr(js, result, "data", dataArr);

    // Clean up
    QRcode_free(qrcode);
    if (must_free_data) {
        JS_FreeCString(js, data);
    }

    return result;
}

static uint8_t rgba_to_gray(uint8_t r, uint8_t g, uint8_t b)
{
  return (uint8_t)((  299 * r + 587 * g + 114 * b + 500) / 1000);
}

// QR decode function (unchanged)
static JSValue js_qr_decode(JSContext *js, JSValueConst this_val, int argc, JSValueConst *argv) {
  size_t data_len;
  uint8_t *src;
  int w, h, pitch;
  src = js_get_blob_data(js, &data_len, argv[0]);
  
  if (!src)
    return JS_ThrowTypeError(js, "decode expects an ArrayBuffer");
    
  JS_ToInt32(js, &w, argv[1]);
  JS_ToInt32(js, &h, argv[2]);
  JS_ToInt32(js, &pitch, argv[3]);
  
  if (w <= 0 || h <= 0)
    return JS_ThrowInternalError(js, "Bad width or height: %dx%d", w, h);
    
  struct quirc *qr = quirc_new();
  if (!qr)
    return JS_ThrowInternalError(js, "Failed to initialize QR decoder");
    
  if (quirc_resize(qr, w, h) < 0) {
    quirc_destroy(qr);
    return JS_ThrowInternalError(js, "quirc_resize failed");
  }
  
  uint8_t *dst = quirc_begin(qr, NULL, NULL);  
  
  printf("decoding image size %dx%d, pitch %d and size %d\n", w, h, pitch, data_len);
  
  for (int y = 0; y < h; ++y) {
    uint8_t *row = src + y * pitch;
    uint8_t *out      = dst + y * w;
    for (int x = 0; x < w; ++x) {
      uint8_t a = row[x*4 + 3];             /* alpha */
      if (a < 128) {                        /* mostly transparent */
        out[x] = 255;
      } else {
        uint8_t r = row[x*4+0];
        uint8_t g = row[x*4+1];
        uint8_t b = row[x*4+2];
        out[x]    = rgba_to_gray(r, g, b);
      }
    }
  }
  
  quirc_end(qr);
  
  int count = quirc_count(qr);
  JSValue result = JS_NewArray(js);
  
  for (int i = 0; i < count; i++) {
      struct quirc_code code;
      struct quirc_data qdata;
  
      quirc_extract(qr, i, &code);
      int err = quirc_decode(&code, &qdata);
      if (err == QUIRC_SUCCESS) {
          JSValue item = js_new_blob_stoned_copy(js, qdata.payload, qdata.payload_len);
          JS_SetPropertyUint32(js, result, i, item);
      } else {
        printf("QR error: %s\n", quirc_strerror(err));
      }
  }
  
  quirc_destroy(qr);
  return result;
}

static JSValue js_qr_rgba(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
  if (argc < 1)
    return JS_ThrowTypeError(js, "qr.rgba(mods, ...) needs a modules object");

  JSValue mods = argv[0];

  uint32_t on  = 0xFF000000u;   /* black opaque */
  uint32_t off = 0xFFFFFFFFu;   /* white opaque */
  if (argc > 1) JS_ToUint32(js, &on,  argv[1]);
  if (argc > 2) JS_ToUint32(js, &off, argv[2]);

  JSValue js_w = JS_GetPropertyStr(js, mods, "width");
  if (JS_IsNull(js_w)) {
      JS_FreeValue(js, js_w);
      return JS_ThrowTypeError(js, "mods.width missing");
  }
  int32_t m = 0;
  JS_ToInt32(js, &m, js_w);
  JS_FreeValue(js, js_w);

  if (m <= 0)
    return JS_ThrowRangeError(js, "mods.width must be > 0");

  JSValue js_bits = JS_GetPropertyStr(js, mods, "data");
  size_t   len    = JS_ArrayLength(js, js_bits);

  if (len < (size_t)m * (size_t)m) {
    JS_FreeValue(js, js_bits);
    return JS_ThrowRangeError(js, "mods.data too small for width²");
  }

  const int w = m, h = m, pitch = w * 4;
  const size_t bytes = (size_t)h * (size_t)pitch;
  uint8_t *tmp = js_malloc(js, bytes);
  if (!tmp) { JS_FreeValue(js, js_bits); return JS_EXCEPTION; }

  for (int y = 0; y < h; ++y) {
    uint32_t *row = (uint32_t *)(tmp + y * pitch);
    for (int x = 0; x < w; x++) {
      uint32_t idx = y * m + x;
      JSValue vbit = JS_GetPropertyUint32(js, js_bits, idx);
      int on_bit = JS_ToBool(js, vbit);
      JS_FreeValue(js, vbit);
      row[x] = on_bit ? on : off;
    }
  }

  JSValue ab  = js_new_blob_stoned_copy(js, tmp, bytes); /* GC owns copy */
  js_free(js, tmp);
  JS_FreeValue(js, js_bits);                           /* done with src */

  JSValue obj = JS_NewObject(js);
  JS_SetPropertyStr(js, obj, "width",  JS_NewInt32(js, w));
  JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, h));
  JS_SetPropertyStr(js, obj, "pitch",  JS_NewInt32(js, pitch));
  JS_SetPropertyStr(js, obj, "buffer", ab);
  JS_SetPropertyStr(js, obj, "format", JS_NewString(js, "rgba32"));
  return obj;
}

// Exported functions for the module
static const JSCFunctionListEntry js_qr_funcs[] = {
    JS_CFUNC_DEF("encode", 2, js_qr_encode),  // Updated to expect 2 args
    JS_CFUNC_DEF("decode", 4, js_qr_decode),
    JS_CFUNC_DEF("rgba", 3, js_qr_rgba),
};

// Helper to return the default export object
JSValue js_qr_use(JSContext *js) {
    JSValue exports = JS_NewObject(js);
    JS_SetPropertyFunctionList(js, exports, js_qr_funcs, sizeof(js_qr_funcs) / sizeof(JSCFunctionListEntry));
    return exports;
}
