#include "qjs_math.h"
#include "jsffi.h"
#include "qjs_macros.h"

#include <math.h>
#include <stdlib.h>
#include "HandmadeMath.h"
#include "cell.h"


// Utility function to get number from array index
static double js_getnum_uint32(JSContext *js, JSValue v, unsigned int i)
{
  JSValue val = JS_GetPropertyUint32(js,v,i);
  double ret = js2number(js, val);
  JS_FreeValue(js,val);
  return ret;
}

// Convert JS array to float array
static float *js2floats(JSContext *js, JSValue v, size_t *len)
{
  *len = JS_ArrayLength(js,v);
  float *arr = malloc(sizeof(float)* *len);
  for (int i = 0; i < *len; i++)
    arr[i] = js_getnum_uint32(js,v,i);
  return arr;
}

// Convert float array to JS array
static JSValue floats2array(JSContext *js, float *vals, size_t len) {
  JSValue arr = JS_NewArray(js);
  for (size_t i = 0; i < len; i++) {
    JS_SetPropertyUint32(js, arr, i, number2js(js, vals[i]));
  }
  return arr;
}

// Calculate vector length
static double arr_vec_length(JSContext *js,JSValue v)
{
  int len = JS_ArrayLength(js,v);
  switch(len) {
    case 2: return HMM_LenV2(js2vec2(js,v));
    case 3: return HMM_LenV3(js2vec3(js,v));
    case 4: return HMM_LenV4(js2vec4(js,v));
  }

  double sum = 0;
  for (int i = 0; i < len; i++)
    sum += pow(js_getnum_uint32(js, v, i), 2);

  return sqrt(sum);
}


// GCD helper function
static int gcd(int a, int b) {
    if (b == 0)
        return a;
    return gcd(b, a % b);
}

// MATH FUNCTIONS



JSC_CCALL(math_rotate,
  HMM_Vec2 vec = js2vec2(js,argv[0]);
  double angle = js2angle(js, argv[1]);
  HMM_Vec2 pivot = JS_IsNull(argv[2]) ? v2zero : js2vec2(js,argv[2]);  
  vec = HMM_SubV2(vec,pivot);

  float r = HMM_LenV2(vec);
  angle += atan2f(vec.y, vec.x);

  vec.x = r * cosf(angle);
  vec.y = r * sinf(angle);

  vec = HMM_AddV2(vec,pivot);

  return vec22js(js, vec);
)

JSC_CCALL(math_norm,
  int len = JS_ArrayLength(js,argv[0]);

  switch(len) {
    case 2: return vec22js(js,HMM_NormV2(js2vec2(js,argv[0])));
    case 3: return vec32js(js, HMM_NormV3(js2vec3(js,argv[0])));
    case 4: return vec42js(js,HMM_NormV4(js2vec4(js,argv[0])));
  }

  double length = arr_vec_length(js,argv[0]);
  JSValue newarr = JS_NewArray(js);

  for (int i = 0; i < len; i++)
    JS_SetPropertyUint32(js, newarr, i, number2js(js,js_getnum_uint32(js, argv[0],i)/length));

  ret = newarr;
)

JSC_CCALL(math_angle_between,
  int len = JS_ArrayLength(js,argv[0]);
  switch(len) {
    case 2: return angle2js(js,HMM_AngleV2(js2vec2(js,argv[0]), js2vec2(js,argv[1])));
    case 3: return angle2js(js,HMM_AngleV3(js2vec3(js,argv[0]), js2vec3(js,argv[1])));
    case 4: return angle2js(js,HMM_AngleV4(js2vec4(js,argv[0]), js2vec4(js,argv[1])));
  }
  return JS_ThrowReferenceError(js, "Input array must have a length between 2 and 4.");
)

JSC_CCALL(math_lerp,
  double s = js2number(js,argv[0]);
  double f = js2number(js,argv[1]);
  double t = js2number(js,argv[2]);
  
  ret = number2js(js,(f-s)*t+s);
)

JSC_CCALL(math_gcd, ret = number2js(js,gcd(js2number(js,argv[0]), js2number(js,argv[1]))); )

JSC_CCALL(math_lcm,
  double a = js2number(js,argv[0]);
  double b = js2number(js,argv[1]);
  ret = number2js(js,(a*b)/gcd(a,b));
)

JSC_CCALL(math_clamp,
  double x = js2number(js,argv[0]);
  double l = js2number(js,argv[1]);
  double h = js2number(js,argv[2]);
  return number2js(js,x > h ? h : x < l ? l : x);
)

JSC_CCALL(math_angledist,
  double a1 = js2number(js,argv[0]);
  double a2 = js2number(js,argv[1]);
  a1 = fmod(a1,1);
  a2 = fmod(a2,1);
  double dist = a2-a1;
  if (dist == 0) return number2js(js,dist);
  if (dist > 0) {
    if (dist > 0.5) return number2js(js,dist-1);
    return number2js(js,dist);
  }
  
  if (dist < -0.5) return number2js(js,dist+1);
  
  return number2js(js,dist);
)

JSC_CCALL(math_jitter,
  double n = js2number(js,argv[0]);
  double pct = js2number(js,argv[1]);
  
  return number2js(js,n + (rand_range(js,-pct,pct)*n));
)

JSC_CCALL(math_mean,
  double len =  JS_ArrayLength(js,argv[0]);
  double sum = 0;
  for (int i = 0; i < len; i++)
    sum += js_getnum_uint32(js, argv[0], i);
    
  return number2js(js,sum/len);
)

JSC_CCALL(math_sum,
  double sum = 0.0;
  int len = JS_ArrayLength(js,argv[0]);
  for (int i = 0; i < len; i++)
    sum += js_getnum_uint32(js, argv[0], i);
  
  return number2js(js,sum);
)

JSC_CCALL(math_sigma,
  int len = JS_ArrayLength(js,argv[0]);
  double sum = 0;
  for (int i = 0; i < len; i++)
    sum += js_getnum_uint32(js, argv[0], i);
  
  double mean = sum/(double)len;
  sum = 0;
  for (int i = 0; i < len; i++)
    sum += pow(js_getnum_uint32(js, argv[0], i) - mean, 2);
  
  return number2js(js,sqrt(sum/len));
)

JSC_CCALL(math_median,
  int len = JS_ArrayLength(js,argv[0]);
  double vals[len];
  for (int i = 0; i < len; i++)
    vals[i] = js_getnum_uint32(js, argv[0], i);
  
  // Simple bubble sort for median calculation
  for (int i = 0; i < len-1; i++) {
    for (int j = 0; j < len-i-1; j++) {
      if (vals[j] > vals[j+1]) {
        double temp = vals[j];
        vals[j] = vals[j+1];
        vals[j+1] = temp;
      }
    }
  }
  
  if (len % 2 == 0)
    return number2js(js,(vals[len/2-1] + vals[len/2]) / 2.0);
  else
    return number2js(js,vals[len/2]);
)

JSC_CCALL(math_length, return number2js(js,arr_vec_length(js,argv[0])); )

JSC_CCALL(math_from_to,
  double start = js2number(js,argv[0]);
  double end = js2number(js,argv[1]);
  double step = js2number(js,argv[2]);
  int inclusive = JS_ToBool(js,argv[3]);
  int arr = JS_ToBool(js,argv[4]);
  
  JSValue jsarr = JS_NewArray(js);
  int i = 0;
  for (double val = start; val <= end; val += step) {
    if (val == end && !inclusive) break;
    JS_SetPropertyUint32(js, jsarr, i++, number2js(js, val));
  }
  
  return jsarr;
)


JSC_CCALL(math_dot,
  size_t alen, blen;
  float *a = js2floats(js,argv[0], &alen);
  float *b = js2floats(js,argv[1], &blen);
  float dot = 0;
  size_t len = alen < blen? alen : blen;
  for (size_t i = 0; i < len; i++)
    dot += a[i] * b[i];
    
  free(a);
  free(b);
  return number2js(js,dot);
)

JSC_CCALL(math_project,
  size_t alen, blen;
  float *a = js2floats(js, argv[0], &alen);
  float *b = js2floats(js, argv[1], &blen);

  if (!a || !b) {
    free(a);
    free(b);
    return JS_NULL;
  }

  // We'll work up to the smaller length
  size_t len = (alen < blen) ? alen : blen;
  if (len == 0) {
    free(a);
    free(b);
    return JS_NULL;
  }

  // Compute dot products: a·b and b·b
  float ab = 0, bb = 0;
  for (size_t i = 0; i < len; i++) {
    ab += a[i] * b[i];
    bb += b[i] * b[i];
  }

  // Build the result array
  float *proj = (float*)malloc(sizeof(float) * len);
  if (!proj) {
    free(a);
    free(b);
    return JS_EXCEPTION; // or some error
  }

  float scale = (bb != 0.0f) ? (ab / bb) : 0.0f;
  for (size_t i = 0; i < len; i++)
    proj[i] = scale * b[i];

  ret = floats2array(js, proj, len);

  free(a);
  free(b);
  free(proj);
)

JSC_CCALL(math_midpoint,
  size_t alen, blen;
  float *a = js2floats(js, argv[0], &alen);
  float *b = js2floats(js, argv[1], &blen);

  if (!a || !b) {
    free(a);
    free(b);
    return JS_NULL;
  }

  size_t len = (alen < blen) ? alen : blen;
  if (len == 0) {
    free(a);
    free(b);
    return JS_NULL;
  }

  float *m = (float*)malloc(sizeof(float) * len);
  if (!m) {
    free(a);
    free(b);
    return JS_EXCEPTION;
  }

  for (size_t i = 0; i < len; i++)
    m[i] = (a[i] + b[i]) * 0.5f;

  ret = floats2array(js, m, len);

  free(a);
  free(b);
  free(m);
)

JSC_CCALL(math_reflect,
  size_t alen, blen;
  float *a = js2floats(js, argv[0], &alen);
  float *b = js2floats(js, argv[1], &blen);
  
  if (!a || !b) {
    free(a);
    free(b);
    return JS_NULL;
  }
  
  size_t len = (alen < blen) ? alen : blen;
  if (len == 0) {
    free(a);
    free(b);
    return JS_NULL;
  }
  
  // Reflect vector a across normal b
  // reflection = a - 2 * (a·b) * b
  float ab = 0, bb = 0;
  for (size_t i = 0; i < len; i++) {
    ab += a[i] * b[i];
    bb += b[i] * b[i];
  }
  
  float *result = (float*)malloc(sizeof(float) * len);
  if (!result) {
    free(a);
    free(b);
    return JS_EXCEPTION;
  }
  
  float scale = (bb != 0.0f) ? (2.0f * ab / bb) : 0.0f;
  for (size_t i = 0; i < len; i++)
    result[i] = a[i] - scale * b[i];
  
  ret = floats2array(js, result, len);
  
  free(a);
  free(b);
  free(result);
)

JSC_CCALL(math_direction,
  size_t alen, blen;
  float *a = js2floats(js, argv[0], &alen);
  float *b = js2floats(js, argv[1], &blen);
  
  if (!a || !b) {
    free(a);
    free(b);
    return JS_NULL;
  }
  
  size_t len = (alen < blen) ? alen : blen;
  if (len == 0) {
    free(a);
    free(b);
    return JS_NULL;
  }
  
  // Direction vector from a to b (normalized)
  float *dir = (float*)malloc(sizeof(float) * len);
  if (!dir) {
    free(a);
    free(b);
    return JS_EXCEPTION;
  }
  
  float mag = 0;
  for (size_t i = 0; i < len; i++) {
    dir[i] = b[i] - a[i];
    mag += dir[i] * dir[i];
  }
  
  mag = sqrtf(mag);
  if (mag > 0.0f) {
    for (size_t i = 0; i < len; i++)
      dir[i] /= mag;
  }
  
  ret = floats2array(js, dir, len);
  
  free(a);
  free(b);
  free(dir);
)

JSC_CCALL(math_angle,
  size_t len;
  float *v = js2floats(js, argv[0], &len);
  
  if (!v || len < 2) {
    free(v);
    return JS_NULL;
  }
  
  // Return angle in radians for 2D vector
  ret = number2js(js, atan2f(v[1], v[0]));
  
  free(v);
)

JSC_CCALL(math_distance,
  size_t alen, blen;
  float *a = js2floats(js, argv[0], &alen);
  float *b = js2floats(js, argv[1], &blen);

  if (!a || !b) {
    free(a);
    free(b);
    return JS_NULL;
  }

  size_t len = (alen < blen) ? alen : blen;
  if (len == 0) {
    free(a);
    free(b);
    return JS_NULL;
  }

  float distSq = 0.0f;
  for (size_t i = 0; i < len; i++) {
    float diff = b[i] - a[i];
    distSq += diff * diff;
  }
  float dist = sqrtf(distSq);

  free(a);
  free(b);
  return number2js(js, dist);
)

static const JSCFunctionListEntry js_math_funcs[] = {
  MIST_FUNC_DEF(math, dot,2),
  MIST_FUNC_DEF(math, project,2),
  MIST_FUNC_DEF(math, rotate, 3),
  MIST_FUNC_DEF(math, midpoint, 2),
  MIST_FUNC_DEF(math, reflect, 2),
  MIST_FUNC_DEF(math, distance, 2),
  MIST_FUNC_DEF(math, direction, 2),
  MIST_FUNC_DEF(math, angle, 1),
  MIST_FUNC_DEF(math, norm, 1),
  MIST_FUNC_DEF(math, angle_between, 2),
  MIST_FUNC_DEF(math, lerp, 3),
  MIST_FUNC_DEF(math, gcd, 2),
  MIST_FUNC_DEF(math, lcm, 2),
  MIST_FUNC_DEF(math, clamp, 3),
  MIST_FUNC_DEF(math, angledist, 2),
  MIST_FUNC_DEF(math, jitter, 2),
  MIST_FUNC_DEF(math, mean, 1),
  MIST_FUNC_DEF(math, sum, 1),
  MIST_FUNC_DEF(math, sigma, 1),
  MIST_FUNC_DEF(math, median, 1),
  MIST_FUNC_DEF(math, length, 1),
  MIST_FUNC_DEF(math, from_to, 5),
};

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