#include "quickjs.h"
#include <string.h>
#include <math.h>

#ifdef __APPLE__
#include <Accelerate/Accelerate.h>
#else
#include <cblas.h>
#include <lapacke.h>
#endif

static JSClassID js_matrix_class_id;
static JSClassID js_array_class_id;

typedef struct {
    double *data;
    int rows;
    int cols;
} matrix_t;

typedef struct {
    double *data;
    int size;
} array_t;

static void js_matrix_finalizer(JSRuntime *rt, JSValue val) {
    matrix_t *mat = JS_GetOpaque(val, js_matrix_class_id);
    if (mat) {
        js_free_rt(rt, mat->data);
        js_free_rt(rt, mat);
    }
}

static void js_array_finalizer(JSRuntime *rt, JSValue val) {
    array_t *arr = JS_GetOpaque(val, js_array_class_id);
    if (arr) {
        js_free_rt(rt, arr->data);
        js_free_rt(rt, arr);
    }
}

static JSClassDef js_matrix_class = {
    "Matrix",
    .finalizer = js_matrix_finalizer,
};

static JSClassDef js_array_class = {
    "Array",
    .finalizer = js_array_finalizer,
};

static matrix_t *js2matrix(JSContext *ctx, JSValue v) {
    return JS_GetOpaque(v, js_matrix_class_id);
}

static array_t *js2array(JSContext *ctx, JSValue v) {
    return JS_GetOpaque(v, js_array_class_id);
}

static JSValue js_matrix_constructor(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "Matrix constructor requires an array argument");
    
    if (!JS_IsArray(ctx, argv[0]))
        return JS_ThrowTypeError(ctx, "Matrix constructor requires an array argument");
    
    JSValue length_val = JS_GetPropertyStr(ctx, argv[0], "length");
    int32_t rows;
    JS_ToInt32(ctx, &rows, length_val);
    JS_FreeValue(ctx, length_val);
    
    if (rows == 0)
        return JS_ThrowRangeError(ctx, "Matrix cannot have 0 rows");
    
    // Get first row to determine columns
    JSValue first_row = JS_GetPropertyUint32(ctx, argv[0], 0);
    if (!JS_IsArray(ctx, first_row)) {
        JS_FreeValue(ctx, first_row);
        return JS_ThrowTypeError(ctx, "Matrix rows must be arrays");
    }
    
    JSValue col_length_val = JS_GetPropertyStr(ctx, first_row, "length");
    int32_t cols;
    JS_ToInt32(ctx, &cols, col_length_val);
    JS_FreeValue(ctx, col_length_val);
    JS_FreeValue(ctx, first_row);
    
    if (cols == 0)
        return JS_ThrowRangeError(ctx, "Matrix cannot have 0 columns");
    
    matrix_t *mat = js_malloc(ctx, sizeof(matrix_t));
    if (!mat)
        return JS_EXCEPTION;
    
    mat->rows = rows;
    mat->cols = cols;
    mat->data = js_malloc(ctx, sizeof(double) * rows * cols);
    if (!mat->data) {
        js_free(ctx, mat);
        return JS_EXCEPTION;
    }
    
    // Fill matrix data
    for (int i = 0; i < rows; i++) {
        JSValue row = JS_GetPropertyUint32(ctx, argv[0], i);
        if (!JS_IsArray(ctx, row)) {
            js_free(ctx, mat->data);
            js_free(ctx, mat);
            JS_FreeValue(ctx, row);
            return JS_ThrowTypeError(ctx, "All matrix rows must be arrays");
        }
        
        JSValue row_length_val = JS_GetPropertyStr(ctx, row, "length");
        int32_t row_cols;
        JS_ToInt32(ctx, &row_cols, row_length_val);
        JS_FreeValue(ctx, row_length_val);
        
        if (row_cols != cols) {
            js_free(ctx, mat->data);
            js_free(ctx, mat);
            JS_FreeValue(ctx, row);
            return JS_ThrowTypeError(ctx, "All matrix rows must have the same length");
        }
        
        for (int j = 0; j < cols; j++) {
            JSValue elem = JS_GetPropertyUint32(ctx, row, j);
            double val;
            if (JS_ToFloat64(ctx, &val, elem)) {
                js_free(ctx, mat->data);
                js_free(ctx, mat);
                JS_FreeValue(ctx, elem);
                JS_FreeValue(ctx, row);
                return JS_EXCEPTION;
            }
            mat->data[i * cols + j] = val;
            JS_FreeValue(ctx, elem);
        }
        JS_FreeValue(ctx, row);
    }
    
    JSValue obj = JS_NewObjectClass(ctx, js_matrix_class_id);
    JS_SetOpaque(obj, mat);
    return obj;
}

static JSValue js_array_constructor(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "Array constructor requires an array argument");
    
    if (!JS_IsArray(ctx, argv[0]))
        return JS_ThrowTypeError(ctx, "Array constructor requires an array argument");
    
    JSValue length_val = JS_GetPropertyStr(ctx, argv[0], "length");
    int32_t size;
    JS_ToInt32(ctx, &size, length_val);
    JS_FreeValue(ctx, length_val);
    
    if (size == 0)
        return JS_ThrowRangeError(ctx, "Array cannot have 0 elements");
    
    array_t *arr = js_malloc(ctx, sizeof(array_t));
    if (!arr)
        return JS_EXCEPTION;
    
    arr->size = size;
    arr->data = js_malloc(ctx, sizeof(double) * size);
    if (!arr->data) {
        js_free(ctx, arr);
        return JS_EXCEPTION;
    }
    
    // Fill array data
    for (int i = 0; i < size; i++) {
        JSValue elem = JS_GetPropertyUint32(ctx, argv[0], i);
        double val;
        if (JS_ToFloat64(ctx, &val, elem)) {
            js_free(ctx, arr->data);
            js_free(ctx, arr);
            JS_FreeValue(ctx, elem);
            return JS_EXCEPTION;
        }
        arr->data[i] = val;
        JS_FreeValue(ctx, elem);
    }
    
    JSValue obj = JS_NewObjectClass(ctx, js_array_class_id);
    JS_SetOpaque(obj, arr);
    return obj;
}

static JSValue js_matrix_inverse(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    matrix_t *mat = js2matrix(ctx, this_val);
    if (!mat)
        return JS_EXCEPTION;
    
    if (mat->rows != mat->cols)
        return JS_ThrowTypeError(ctx, "Matrix must be square for inversion");
    
    int n = mat->rows;
    
    // Create a copy of the matrix
    matrix_t *result = js_malloc(ctx, sizeof(matrix_t));
    if (!result)
        return JS_EXCEPTION;
    
    result->rows = n;
    result->cols = n;
    result->data = js_malloc(ctx, sizeof(double) * n * n);
    if (!result->data) {
        js_free(ctx, result);
        return JS_EXCEPTION;
    }
    
    memcpy(result->data, mat->data, sizeof(double) * n * n);
    
    // Allocate pivot array
    int *ipiv = js_malloc(ctx, sizeof(int) * n);
    if (!ipiv) {
        js_free(ctx, result->data);
        js_free(ctx, result);
        return JS_EXCEPTION;
    }
    
    // LU decomposition
#ifdef __APPLE__
    // For macOS with ILP64, use 64-bit integers
    __LAPACK_int nn = n;
    __LAPACK_int info = 0;
    __LAPACK_int *ipiv_lapack = js_malloc(ctx, sizeof(__LAPACK_int) * n);
    if (!ipiv_lapack) {
        js_free(ctx, ipiv);
        js_free(ctx, result->data);
        js_free(ctx, result);
        return JS_EXCEPTION;
    }
    
    // LAPACK uses column-major, but we'll transpose the result
    __LAPACK_int lda = n;
    dgetrf_(&nn, &nn, result->data, &lda, ipiv_lapack, &info);
    
    if (info != 0) {
        js_free(ctx, ipiv_lapack);
        js_free(ctx, ipiv);
        js_free(ctx, result->data);
        js_free(ctx, result);
        return JS_ThrowInternalError(ctx, "Matrix LU decomposition failed");
    }
    
    // Compute inverse
    __LAPACK_int lwork = n * n;
    double *work = js_malloc(ctx, sizeof(double) * lwork);
    if (!work) {
        js_free(ctx, ipiv_lapack);
        js_free(ctx, ipiv);
        js_free(ctx, result->data);
        js_free(ctx, result);
        return JS_EXCEPTION;
    }
    
    dgetri_(&nn, result->data, &lda, ipiv_lapack, work, &lwork, &info);
    js_free(ctx, work);
    js_free(ctx, ipiv_lapack);
#else
    int info = LAPACKE_dgetrf(LAPACK_ROW_MAJOR, n, n, result->data, n, ipiv);
    if (info != 0) {
        js_free(ctx, ipiv);
        js_free(ctx, result->data);
        js_free(ctx, result);
        return JS_ThrowInternalError(ctx, "Matrix LU decomposition failed");
    }
    
    // Compute inverse
    info = LAPACKE_dgetri(LAPACK_ROW_MAJOR, n, result->data, n, ipiv);
#endif
    js_free(ctx, ipiv);
    
    if (info != 0) {
        js_free(ctx, result->data);
        js_free(ctx, result);
        return JS_ThrowInternalError(ctx, "Matrix inversion failed");
    }
    
    JSValue obj = JS_NewObjectClass(ctx, js_matrix_class_id);
    JS_SetOpaque(obj, result);
    return obj;
}

static JSValue js_matrix_multiply(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "multiply requires an argument");
    
    matrix_t *A = js2matrix(ctx, this_val);
    if (!A)
        return JS_EXCEPTION;
    
    // Check if multiplying by another matrix
    matrix_t *B = js2matrix(ctx, argv[0]);
    if (B) {
        if (A->cols != B->rows)
            return JS_ThrowTypeError(ctx, "Matrix dimensions incompatible for multiplication");
        
        matrix_t *C = js_malloc(ctx, sizeof(matrix_t));
        if (!C)
            return JS_EXCEPTION;
        
        C->rows = A->rows;
        C->cols = B->cols;
        C->data = js_malloc(ctx, sizeof(double) * C->rows * C->cols);
        if (!C->data) {
            js_free(ctx, C);
            return JS_EXCEPTION;
        }
        
        // C = A * B
        cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
                    A->rows, B->cols, A->cols,
                    1.0, A->data, A->cols,
                    B->data, B->cols,
                    0.0, C->data, C->cols);
        
        JSValue obj = JS_NewObjectClass(ctx, js_matrix_class_id);
        JS_SetOpaque(obj, C);
        return obj;
    }
    
    // Check if multiplying by an array (matrix-vector multiplication)
    array_t *x = js2array(ctx, argv[0]);
    if (x) {
        if (A->cols != x->size)
            return JS_ThrowTypeError(ctx, "Matrix columns must match array size");
        
        array_t *y = js_malloc(ctx, sizeof(array_t));
        if (!y)
            return JS_EXCEPTION;
        
        y->size = A->rows;
        y->data = js_malloc(ctx, sizeof(double) * y->size);
        if (!y->data) {
            js_free(ctx, y);
            return JS_EXCEPTION;
        }
        
        // y = A * x
        cblas_dgemv(CblasRowMajor, CblasNoTrans,
                    A->rows, A->cols,
                    1.0, A->data, A->cols,
                    x->data, 1,
                    0.0, y->data, 1);
        
        JSValue obj = JS_NewObjectClass(ctx, js_array_class_id);
        JS_SetOpaque(obj, y);
        return obj;
    }
    
    return JS_ThrowTypeError(ctx, "multiply requires a Matrix or Array argument");
}

static JSValue js_matrix_to_array(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    matrix_t *mat = js2matrix(ctx, this_val);
    if (!mat)
        return JS_EXCEPTION;
    
    JSValue arr = JS_NewArray(ctx);
    if (JS_IsException(arr))
        return arr;
    
    for (int i = 0; i < mat->rows; i++) {
        JSValue row = JS_NewArray(ctx);
        if (JS_IsException(row)) {
            JS_FreeValue(ctx, arr);
            return row;
        }
        for (int j = 0; j < mat->cols; j++) {
            JS_SetPropertyUint32(ctx, row, j, JS_NewFloat64(ctx, mat->data[i * mat->cols + j]));
        }
        JS_SetPropertyUint32(ctx, arr, i, row);
    }
    return arr;
}

static JSValue js_array_to_array(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    array_t *arr = js2array(ctx, this_val);
    if (!arr)
        return JS_EXCEPTION;
    
    JSValue jsarr = JS_NewArray(ctx);
    for (int i = 0; i < arr->size; i++) {
        JS_SetPropertyUint32(ctx, jsarr, i, JS_NewFloat64(ctx, arr->data[i]));
    }
    return jsarr;
}

static JSValue js_array_dot(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "dot requires an argument");
    
    array_t *a = js2array(ctx, this_val);
    if (!a)
        return JS_EXCEPTION;
    
    array_t *b = js2array(ctx, argv[0]);
    if (!b)
        return JS_ThrowTypeError(ctx, "dot requires an Array argument");
    
    if (a->size != b->size)
        return JS_ThrowTypeError(ctx, "Arrays must have the same size for dot product");
    
    double result = cblas_ddot(a->size, a->data, 1, b->data, 1);
    return JS_NewFloat64(ctx, result);
}

static JSValue js_array_norm(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    array_t *arr = js2array(ctx, this_val);
    if (!arr)
        return JS_EXCEPTION;
    
    double norm = cblas_dnrm2(arr->size, arr->data, 1);
    return JS_NewFloat64(ctx, norm);
}

static JSValue js_array_multiply(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "multiply requires an argument");
    
    array_t *arr = js2array(ctx, this_val);
    if (!arr)
        return JS_EXCEPTION;
    
    // Create result array
    array_t *result = js_malloc(ctx, sizeof(array_t));
    if (!result)
        return JS_EXCEPTION;
    
    result->size = arr->size;
    result->data = js_malloc(ctx, sizeof(double) * result->size);
    if (!result->data) {
        js_free(ctx, result);
        return JS_EXCEPTION;
    }
    
    // Copy original data
    memcpy(result->data, arr->data, sizeof(double) * arr->size);
    
    // Check if multiplying by scalar
    if (JS_IsNumber(argv[0])) {
        double scalar;
        if (JS_ToFloat64(ctx, &scalar, argv[0]))
            return JS_EXCEPTION;
        
        // Multiply each element by scalar
        if (result->size <= 4)
          for (int i = 0; i < result->size; i++)
            result->data[i] *= scalar;
        else
          cblas_dscal(result->size, scalar, result->data, 1);
    } else {
        js_free(ctx, result->data);
        js_free(ctx, result);
        return JS_ThrowTypeError(ctx, "multiply requires a number argument");
    }
    
    JSValue obj = JS_NewObjectClass(ctx, js_array_class_id);
    JS_SetOpaque(obj, result);
    return obj;
}

static JSValue js_array_add(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "add requires an argument");
    
    array_t *arr = js2array(ctx, this_val);
    if (!arr)
        return JS_EXCEPTION;
    
    // Create result array
    array_t *result = js_malloc(ctx, sizeof(array_t));
    if (!result)
        return JS_EXCEPTION;
    
    result->size = arr->size;
    result->data = js_malloc(ctx, sizeof(double) * result->size);
    if (!result->data) {
        js_free(ctx, result);
        return JS_EXCEPTION;
    }
    
    // Copy original data
    memcpy(result->data, arr->data, sizeof(double) * arr->size);
    
    // Check if adding scalar
    if (JS_IsNumber(argv[0])) {
        double scalar;
        if (JS_ToFloat64(ctx, &scalar, argv[0]))
            return JS_EXCEPTION;
        
        // Add scalar to each element
        for (int i = 0; i < result->size; i++)
            result->data[i] += scalar;
    } else {
        // Check if adding another array
        array_t *other = js2array(ctx, argv[0]);
        if (other) {
            if (arr->size != other->size) {
                js_free(ctx, result->data);
                js_free(ctx, result);
                return JS_ThrowTypeError(ctx, "Arrays must have the same size for element-wise addition");
            }
            
            if (arr->size <= 4) {
              for (int i = 0; i < arr->size; i++)
                result->data[i] += other->data[i];
            } else
              cblas_daxpy(result->size, 1.0, other->data, 1, result->data, 1);
        } else {
            js_free(ctx, result->data);
            js_free(ctx, result);
            return JS_ThrowTypeError(ctx, "add requires a number or Array argument");
        }
    }
    
    JSValue obj = JS_NewObjectClass(ctx, js_array_class_id);
    JS_SetOpaque(obj, result);
    return obj;
}

static JSValue js_array_slice(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
  array_t *arr = js2array(js, self);
  if (!arr)
    return JS_EXCEPTION;
  
  int32_t start = 0;
  int32_t end = arr->size;
  
  // Parse start argument
  if (argc >= 1 && !JS_IsNull(argv[0])) {
    if (JS_ToInt32(js, &start, argv[0]))
      return JS_EXCEPTION;
    
    // Handle negative indices
    if (start < 0) {
      start = arr->size + start;
      if (start < 0) start = 0;
    } else if (start > arr->size) {
      start = arr->size;
    }
  }
  
  // Parse end argument
  if (argc >= 2 && !JS_IsNull(argv[1])) {
    if (JS_ToInt32(js, &end, argv[1]))
      return JS_EXCEPTION;
    
    // Handle negative indices
    if (end < 0) {
      end = arr->size + end;
      if (end < 0) end = 0;
    } else if (end > arr->size) {
      end = arr->size;
    }
  }
  
  // Ensure start <= end
  if (start > end) {
    end = start;  // Return empty array
  }
  
  // Create JavaScript array
  JSValue jsarr = JS_NewArray(js);
  if (JS_IsException(jsarr))
    return jsarr;
  
  // Fill with sliced values
  for (int32_t i = start; i < end; i++) {
    JS_SetPropertyUint32(js, jsarr, i - start, JS_NewFloat64(js, arr->data[i]));
  }
  
  return jsarr;
}

static JSValue js_array_multiplyf(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "multiplyf requires an argument");
    
    array_t *arr = js2array(ctx, this_val);
    if (!arr)
        return JS_EXCEPTION;
    
    // Check if multiplying by scalar
    if (JS_IsNumber(argv[0])) {
        double scalar;
        if (JS_ToFloat64(ctx, &scalar, argv[0]))
            return JS_EXCEPTION;
        
        // Multiply each element by scalar in-place
        if (arr->size <= 4) {
            for (int i = 0; i < arr->size; i++)
                arr->data[i] *= scalar;
        } else {
            cblas_dscal(arr->size, scalar, arr->data, 1);
        }
        
        return JS_DupValue(ctx, this_val);
    } else {
        return JS_ThrowTypeError(ctx, "multiplyf requires a number argument");
    }
}

static JSValue js_array_addf(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
    if (argc < 1)
        return JS_ThrowTypeError(ctx, "addf requires an argument");
    
    array_t *arr = js2array(ctx, this_val);
    if (!arr)
        return JS_EXCEPTION;
    
    // Check if adding scalar
    if (JS_IsNumber(argv[0])) {
        double scalar;
        if (JS_ToFloat64(ctx, &scalar, argv[0]))
            return JS_EXCEPTION;
        
        // Add scalar to each element in-place
        for (int i = 0; i < arr->size; i++)
            arr->data[i] += scalar;
    } else {
        // Check if adding another array
        array_t *other = js2array(ctx, argv[0]);
        if (other) {
            if (arr->size != other->size) {
                return JS_ThrowTypeError(ctx, "Arrays must have the same size for element-wise addition");
            }
            
            // Add arrays element-wise in-place
            if (arr->size <= 4) {
                for (int i = 0; i < arr->size; i++)
                    arr->data[i] += other->data[i];
            } else {
                cblas_daxpy(arr->size, 1.0, other->data, 1, arr->data, 1);
            }
        } else {
            return JS_ThrowTypeError(ctx, "addf requires a number or Array argument");
        }
    }
    
    return JS_DupValue(ctx, this_val);
}



static const JSCFunctionListEntry js_matrix_proto_funcs[] = {
    JS_CFUNC_DEF("inverse", 0, js_matrix_inverse),
    JS_CFUNC_DEF("multiply", 1, js_matrix_multiply),
    JS_CFUNC_DEF("toArray", 0, js_matrix_to_array),
};

static const JSCFunctionListEntry js_array_proto_funcs[] = {
    JS_CFUNC_DEF("dot", 1, js_array_dot),
    JS_CFUNC_DEF("norm", 0, js_array_norm),
    JS_CFUNC_DEF("multiply", 1, js_array_multiply),
    JS_CFUNC_DEF("add", 1, js_array_add),
    JS_CFUNC_DEF("multiplyf", 1, js_array_multiplyf),
    JS_CFUNC_DEF("addf", 1, js_array_addf),
    JS_CFUNC_DEF("toArray", 0, js_array_to_array),
    JS_CFUNC_DEF("toJSON",  0, js_array_to_array),  /* ← for JSON.stringify */
    JS_CFUNC_DEF("slice", 2, js_array_slice),
};

JSValue js_num_use(JSContext *ctx) {
    // Register Matrix class
    JS_NewClassID(&js_matrix_class_id);
    JS_NewClass(JS_GetRuntime(ctx), js_matrix_class_id, &js_matrix_class);
    JSValue matrix_proto = JS_NewObject(ctx);
    JS_SetPropertyFunctionList(ctx, matrix_proto, js_matrix_proto_funcs, 
                               sizeof(js_matrix_proto_funcs) / sizeof(JSCFunctionListEntry));
    JS_SetClassProto(ctx, js_matrix_class_id, matrix_proto);
    
    // Create Matrix constructor
    JSValue matrix_ctor = JS_NewCFunction2(ctx, js_matrix_constructor, "Matrix", 1, JS_CFUNC_constructor, 0);
    JS_SetConstructor(ctx, matrix_ctor, matrix_proto);
    
    // Register Array class
    JS_NewClassID(&js_array_class_id);
    JS_NewClass(JS_GetRuntime(ctx), js_array_class_id, &js_array_class);
    JSValue array_proto = JS_NewObject(ctx);
    JS_SetPropertyFunctionList(ctx, array_proto, js_array_proto_funcs,
                               sizeof(js_array_proto_funcs) / sizeof(JSCFunctionListEntry));
    JS_SetClassProto(ctx, js_array_class_id, array_proto);
    
    // Create Array constructor
    JSValue array_ctor = JS_NewCFunction2(ctx, js_array_constructor, "Array", 1, JS_CFUNC_constructor, 0);
    JS_SetConstructor(ctx, array_ctor, array_proto);
    
    JSValue export = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, export, "Matrix", matrix_ctor);
    JS_SetPropertyStr(ctx, export, "Array", array_ctor);
    return export;
}

