#include "cell.h"


#if defined(_WIN32)
#include <windows.h>
#include <winhttp.h>
#include <stdio.h>

typedef unsigned char par_byte;

static void par_easycurl_init(unsigned int flags) {
  (void)flags;
}

static int par_easycurl_to_memory(char const *url, par_byte **data, int *nbytes) {
  if (!url || !data || !nbytes) return 0;
  *data = NULL;
  *nbytes = 0;

  int success = 0;
  HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL;
  wchar_t *wUrl = NULL, *wHost = NULL, *wPath = NULL;
  char *buffer = NULL;
  int bufferSize = 0;

  int len = MultiByteToWideChar(CP_UTF8, 0, url, -1, NULL, 0);
  if (len <= 0) goto cleanup;
  wUrl = (wchar_t *)malloc(len * sizeof(wchar_t));
  if (!wUrl) goto cleanup;
  MultiByteToWideChar(CP_UTF8, 0, url, -1, wUrl, len);

  URL_COMPONENTS urlComp;
  ZeroMemory(&urlComp, sizeof(urlComp));
  urlComp.dwStructSize = sizeof(urlComp);
  urlComp.dwHostNameLength = (DWORD)-1;
  urlComp.dwUrlPathLength = (DWORD)-1;

  if (!WinHttpCrackUrl(wUrl, (DWORD)wcslen(wUrl), 0, &urlComp)) goto cleanup;

  wHost = (wchar_t *)malloc((urlComp.dwHostNameLength + 1) * sizeof(wchar_t));
  if (!wHost) goto cleanup;
  wcsncpy(wHost, urlComp.lpszHostName, urlComp.dwHostNameLength);
  wHost[urlComp.dwHostNameLength] = L'\0';

  wPath = (wchar_t *)malloc((urlComp.dwUrlPathLength + 1) * sizeof(wchar_t));
  if (!wPath) goto cleanup;
  wcsncpy(wPath, urlComp.lpszUrlPath, urlComp.dwUrlPathLength);
  wPath[urlComp.dwUrlPathLength] = L'\0';

  hSession = WinHttpOpen(L"cell/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                         WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
  if (!hSession) goto cleanup;

  hConnect = WinHttpConnect(hSession, wHost, urlComp.nPort, 0);
  if (!hConnect) goto cleanup;

  DWORD dwFlags = (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0;

  hRequest = WinHttpOpenRequest(hConnect, L"GET", wPath, NULL,
                                WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags);
  if (!hRequest) goto cleanup;

  if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                          WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) goto cleanup;

  if (!WinHttpReceiveResponse(hRequest, NULL)) goto cleanup;

  DWORD dwStatusCode = 0;
  DWORD dwSize = sizeof(dwStatusCode);
  if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
                           WINHTTP_HEADER_NAME_BY_INDEX, &dwStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX)) {
    goto cleanup;
  }

  if (dwStatusCode >= 400) goto cleanup;

  buffer = (char *)malloc(1);
  if (!buffer) goto cleanup;
  buffer[0] = 0;

  for (;;) {
    DWORD dwAvailable = 0;
    if (!WinHttpQueryDataAvailable(hRequest, &dwAvailable)) goto cleanup;
    if (dwAvailable == 0) break;

    char *newBuffer = (char *)realloc(buffer, bufferSize + dwAvailable + 1);
    if (!newBuffer) goto cleanup;
    buffer = newBuffer;

    DWORD dwDownloaded = 0;
    if (!WinHttpReadData(hRequest, buffer + bufferSize, dwAvailable, &dwDownloaded)) goto cleanup;
    bufferSize += dwDownloaded;
    buffer[bufferSize] = 0;
  }

  *data = (par_byte *)buffer;
  *nbytes = bufferSize;
  success = 1;
  buffer = NULL;

cleanup:
  if (wUrl) free(wUrl);
  if (wHost) free(wHost);
  if (wPath) free(wPath);
  if (buffer) free(buffer);
  if (hRequest) WinHttpCloseHandle(hRequest);
  if (hConnect) WinHttpCloseHandle(hConnect);
  if (hSession) WinHttpCloseHandle(hSession);
  return success;
}

#elif defined(__EMSCRIPTEN__)
#include <emscripten/fetch.h>
#include <emscripten/emscripten.h>
#include <stdio.h>

typedef unsigned char par_byte;

static void par_easycurl_init(unsigned int flags) {
  (void)flags;
}

typedef struct {
  par_byte *data;
  int nbytes;
  int complete;
  int success;
} fetch_result;

static void on_fetch_success(emscripten_fetch_t *fetch) {
  fetch_result *res = (fetch_result *)fetch->userData;
  res->nbytes = fetch->numBytes;
  res->data = (par_byte *)malloc((size_t)res->nbytes + 1);
  if (!res->data) {
    res->success = 0;
  } else {
    memcpy(res->data, fetch->data, (size_t)res->nbytes);
    res->data[res->nbytes] = 0;
    res->success = 1;
  }
  res->complete = 1;
  emscripten_fetch_close(fetch);
}

static void on_fetch_failure(emscripten_fetch_t *fetch) {
  fetch_result *res = (fetch_result *)fetch->userData;
  res->success = 0;
  res->complete = 1;
  emscripten_fetch_close(fetch);
}

static int par_easycurl_to_memory(char const *url, par_byte **data, int *nbytes) {
  if (!url || !data || !nbytes) return 0;
  *data = NULL;
  *nbytes = 0;

  fetch_result res = {0};
  emscripten_fetch_attr_t attr;
  emscripten_fetch_attr_init(&attr);
  strcpy(attr.requestMethod, "GET");
  attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
  attr.userData = &res;
  attr.onsuccess = on_fetch_success;
  attr.onerror = on_fetch_failure;
  attr.attributes |= EMSCRIPTEN_FETCH_SYNCHRONOUS;

  emscripten_fetch_t *fetch = emscripten_fetch(&attr, url);
  if (!fetch) return 0;

  if (!res.complete || !res.success) return 0;

  *data = res.data;
  *nbytes = res.nbytes;
  return 1;
}

#elif defined(__APPLE__)
#include <TargetConditionals.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CFNetwork/CFNetwork.h>
#include <stdio.h>

typedef unsigned char par_byte;

static void par_easycurl_init(unsigned int flags) {
  (void)flags;
}

typedef struct {
  CFMutableDataRef data;
  int success;
  int complete;
} cf_result;

static void cf_response_callback(CFReadStreamRef stream, CFStreamEventType type, void *clientCallBackInfo) {
  cf_result *res = (cf_result *)clientCallBackInfo;
  if (type == kCFStreamEventHasBytesAvailable) {
    UInt8 buffer[4096];
    CFIndex bytesRead = CFReadStreamRead(stream, buffer, sizeof(buffer));
    if (bytesRead > 0) {
      CFDataAppendBytes(res->data, buffer, bytesRead);
    }
  } else if (type == kCFStreamEventEndEncountered) {
    res->success = 1;
    res->complete = 1;
    CFReadStreamClose(stream);
  } else if (type == kCFStreamEventErrorOccurred) {
    res->success = 0;
    res->complete = 1;
    CFReadStreamClose(stream);
  }
}

static int par_easycurl_to_memory(char const *url, par_byte **data, int *nbytes) {
  if (!url || !data || !nbytes) return 0;
  *data = NULL;
  *nbytes = 0;

  CFStringRef cfurl = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
  if (!cfurl) return 0;
  CFURLRef cfurlRef = CFURLCreateWithString(NULL, cfurl, NULL);
  CFRelease(cfurl);
  if (!cfurlRef) return 0;

  CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), cfurlRef, kCFHTTPVersion1_1);
  CFRelease(cfurlRef);
  if (!request) return 0;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
  CFReadStreamRef stream = CFReadStreamCreateForHTTPRequest(NULL, request);
#pragma clang diagnostic pop
  CFRelease(request);
  if (!stream) return 0;

  cf_result res;
  res.data = CFDataCreateMutable(NULL, 0);
  res.success = 0;
  res.complete = 0;
  if (!res.data) {
    CFRelease(stream);
    return 0;
  }

  CFStreamClientContext ctx = {0, &res, NULL, NULL, NULL};
  CFOptionFlags events = kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
  if (!CFReadStreamSetClient(stream, events, cf_response_callback, &ctx)) {
    CFRelease(stream);
    CFRelease(res.data);
    return 0;
  }
  CFReadStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
  if (!CFReadStreamOpen(stream)) {
    CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
    CFRelease(stream);
    CFRelease(res.data);
    return 0;
  }

  while (!res.complete) {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, false);
  }

  CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
  CFRelease(stream);

  if (!res.success) {
    CFRelease(res.data);
    return 0;
  }

  CFIndex len = CFDataGetLength(res.data);
  par_byte *bytes = (par_byte *)malloc((size_t)len + 1);
  if (!bytes) {
    CFRelease(res.data);
    return 0;
  }
  CFDataGetBytes(res.data, CFRangeMake(0, len), bytes);
  bytes[len] = 0;
  CFRelease(res.data);

  *data = bytes;
  *nbytes = (int)len;
  return 1;
}

#else
#define PAR_EASYCURL_IMPLEMENTATION
#include "par_easycurl.h"
#endif

#include <string.h>
#include <stdlib.h>
#include <stdio.h>

// Performs a blocking HTTP GET and returns a QuickJS Blob of the body
static JSValue js_fetch_picoparser(JSContext *ctx, JSValueConst this_val,
                                   int argc, JSValueConst *argv) {
  if (argc < 1 || !JS_IsText(argv[0]))
    return JS_RaiseDisrupt(ctx, "fetch: URL string required");

  const char *url = JS_ToCString(ctx, argv[0]);
  if (!url) return JS_RaiseDisrupt(ctx, "fetch: invalid URL");

  par_byte *data = NULL;
  int nbytes = 0;
  if (!par_easycurl_to_memory(url, &data, &nbytes)) {
    JS_FreeCString(ctx, url);
    return JS_RaiseDisrupt(ctx, "fetch: failed to fetch URL");
  }

  JS_FreeCString(ctx, url);

  // Return a Blob wrapping the data
  JSValue blob = js_new_blob_stoned_copy(ctx, data, (size_t)nbytes);
  free(data);  // par_easycurl allocates with malloc, so we free it
  return blob;
}

static const JSCFunctionListEntry js_http_funcs[] = {
  JS_CFUNC_DEF("fetch", 2, js_fetch_picoparser),
};

JSValue js_core_http_use(JSContext *js) {
  JS_FRAME(js);
  par_easycurl_init(0);  // Initialize platform HTTP backend
  JS_ROOT(mod, JS_NewObject(js));
  JS_SetPropertyFunctionList(js, mod.val, js_http_funcs,
                             sizeof(js_http_funcs)/sizeof(js_http_funcs[0]));
  JS_RETURN(mod.val);
}
